From 2ddfbd51ab6dfed46be2c8fd73b4f4dda290e416 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 12 Sep 2023 14:15:23 -0700 Subject: [PATCH 1/6] 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 | 3 +-- 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/apps/wolfsshd/configuration.c b/apps/wolfsshd/configuration.c index cff184ec8..27fd0f908 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -105,6 +105,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 @@ -429,9 +430,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] = { @@ -458,6 +460,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"}, @@ -1317,6 +1320,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; @@ -1674,6 +1680,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 a294661af..62f91c8e0 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -545,6 +545,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 a03f43686..ebeefc52f 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -1167,6 +1167,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, @@ -1236,10 +1237,8 @@ static int Base16_Decode(const byte* in, word32 inLen, *outLen = outIdx; return 0; } - #endif /* !WOLFSSL_BASE16 */ - static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) { if (b1 != NULL) free(b1); From c94bac9c77f95dd5ab379a44aab5b9a244576529 Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Tue, 26 Sep 2023 16:27:34 -0600 Subject: [PATCH 2/6] 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 62f91c8e0..233eaeabf 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -546,6 +546,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; @@ -576,6 +577,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 b71d1c5b71f761f24bf0afb8509f6b2eda5fec86 Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 11 Oct 2024 15:19:48 -0700 Subject: [PATCH 3/6] 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 27fd0f908..330de1e83 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 */ WOLFSSHD_CONFIG* head; /* global config the Match nodes branch from */ long loginTimer; @@ -106,6 +109,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 @@ -389,6 +393,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; @@ -429,11 +436,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] = { @@ -464,6 +475,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"}, }; @@ -1331,6 +1346,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, @@ -1717,6 +1743,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 233eaeabf..0939d231b 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -545,9 +545,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"); @@ -558,9 +559,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 1e05ae302ed574a88eb622dda892d0dbe9aec4e0 Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 15 Nov 2024 16:02:42 -0700 Subject: [PATCH 4/6] 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 330de1e83..83d9b60e9 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -471,14 +471,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 da9ef19686bf1b2bcb595f46dbfbf4c0591a05fa Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Mon, 9 Feb 2026 15:27:04 -0700 Subject: [PATCH 5/6] 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 | 458 ++++++++-- 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 | 798 ++++++++++++++++-- src/ssh.c | 379 +++++++++ tests/unit.c | 204 ++++- wolfssh/certman.h | 9 + wolfssh/internal.h | 28 + wolfssh/ssh.h | 10 + 24 files changed, 3098 insertions(+), 285 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 1b7a74155..a7429b2ad 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -55,7 +55,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 @@ -2597,10 +2599,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) { #ifdef WOLFSSH_SMALL_STACK DecodedCert* dCert; @@ -2627,6 +2630,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 = @@ -2655,6 +2659,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 " @@ -2692,7 +2705,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 83d9b60e9..4fd4155c4 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 */ WOLFSSHD_CONFIG* head; /* global config the Match nodes branch from */ long loginTimer; @@ -393,9 +400,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; @@ -422,6 +436,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, @@ -438,16 +455,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"}, @@ -459,6 +474,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"}, @@ -475,12 +498,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) @@ -1346,9 +1372,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; @@ -1358,10 +1386,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; } @@ -1725,7 +1782,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; @@ -1761,7 +1822,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."); @@ -1780,12 +1845,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; @@ -1794,24 +1866,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; @@ -1820,23 +1906,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; @@ -1845,17 +1943,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) { @@ -1901,6 +2006,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 0939d231b..54ce66724 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 @@ -340,6 +352,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 */ @@ -382,96 +517,202 @@ 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. */ - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error reading host key file."); - ret = WS_BAD_FILE_E; + 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] 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 (ret == WS_SUCCESS) { - /* Host keys may be OpenSSH, PEM, or DER; detect the format - * and decode PEM/DER via wc_KeyPemToDer(), which handles - * PKCS#8 keys with no traditional DER form (e.g. ML-DSA). */ - 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; + if (wStoreName == NULL || wSubjectName == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Memory allocation failed for cert store strings"); + ret = WS_MEMORY_E; } else { - int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, - heap, &keyDer, &privBuf, &privBufSz); + MultiByteToWideChar(CP_UTF8, 0, hostKeyStore, -1, + wStoreName, storeNameLen); + MultiByteToWideChar(CP_UTF8, 0, hostKeyStoreSubject, -1, + wSubjectName, subjectNameLen); - if (keyFormat == WS_MEMORY_E) { + ret = wolfSSH_CTX_UsePrivateKey_fromStore(*ctx, wStoreName, + dwFlags, wSubjectName); + if (ret != WS_SUCCESS) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Out of memory reading host private key."); - ret = WS_MEMORY_E; + "[SSHD] Failed to load host key from certificate store"); } - else if (keyFormat < 0) { - wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Host private key file is invalid."); + } + + 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); + + 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; + + } + + if (ret == WS_SUCCESS) { + /* Host keys may be OpenSSH, PEM, or DER; detect the format + * and decode PEM/DER via wc_KeyPemToDer(), which handles + * PKCS#8 keys with no traditional DER form (e.g. ML-DSA). */ + 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 (keyFormat == WOLFSSH_FORMAT_OPENSSH) { - wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " - "key as OpenSSH format."); - } else { - wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " - "key as DER format."); - } + int keyFormat = wolfSSHD_DetectPrivKeyFormat(data, dataSz, + heap, &keyDer, &privBuf, &privBufSz); - if (ret == WS_SUCCESS && - wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, - privBufSz, keyFormat) < 0) { - if (keyFormat == WOLFSSH_FORMAT_OPENSSH) { - /* Only composite ML-DSA keys support this format. */ + if (keyFormat == WS_MEMORY_E) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key: " - "OpenSSH format is only supported for " - "composite ML-DSA keys; convert with " - "\"ssh-keygen -p -m PEM\"."); + "[SSHD] Out of memory reading host private key."); + ret = WS_MEMORY_E; } - else { + else if (keyFormat < 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failed to use host private key."); + "[SSHD] Host private key file is invalid."); + ret = WS_BAD_FILE_E; + } + else if (keyFormat == WOLFSSH_FORMAT_OPENSSH) { + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as OpenSSH format."); + } + else { + wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] Loading host private " + "key as DER format."); + } + + if (ret == WS_SUCCESS && + wolfSSH_CTX_UsePrivateKey_buffer(*ctx, privBuf, + privBufSz, keyFormat) < 0) { + if (keyFormat == WOLFSSH_FORMAT_OPENSSH) { + /* Only composite ML-DSA keys support this format. */ + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key: " + "OpenSSH format is only supported for " + "composite ML-DSA keys; convert with " + "\"ssh-keygen -p -m PEM\"."); + } + else { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Failed to use host private key."); + } + ret = WS_BAD_ARGUMENT; } - 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 is the key material itself for raw DER/OpenSSH + * input (privBuf aliases it directly, no copy). */ + WS_FORCEZERO(data, dataSz); + freeBufferFromFile(data, heap); } - /* data is the key material itself for raw DER/OpenSSH - * input (privBuf aliases it directly, no copy). */ - WS_FORCEZERO(data, dataSz); - freeBufferFromFile(data, heap); } } } @@ -545,37 +786,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; } } @@ -592,7 +819,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) { @@ -646,6 +898,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) { @@ -3031,6 +3291,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 + /* Must run before privilege drop so the shadow file is accessible. * Degrades to a fixed-cost fake hash if the shadow read fails. */ if (ret == WS_SUCCESS && !testMode) { @@ -3068,10 +3346,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 279d491c5..efeb4507a 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"], @@ -413,4 +426,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 23ce3896f..164c67b46 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; @@ -1174,7 +1180,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; @@ -1199,3 +1212,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 2dd03dc8c..5c6a62c3f 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 @@ -2995,6 +3006,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"); } @@ -3111,6 +3125,9 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) !defined(WOLFSSH_USER_FILESYSTEM) char* caCert = NULL; #endif + #ifdef WOLFSSH_WINDOWS_CERT_STORE + const char* certStoreSpec = NULL; + #endif int argc = serverArgs->argc; char** argv = serverArgs->argv; @@ -3119,8 +3136,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:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:H"; + const char* optlist = "?1a:d:DefEp: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) { @@ -3250,6 +3270,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; @@ -3454,6 +3480,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) { @@ -3880,7 +3931,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 f78c3306d..9bb6ab4e2 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, userEcc, 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, userEcc, 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 316397e69..b53304465 100644 --- a/src/internal.c +++ b/src/internal.c @@ -80,6 +80,27 @@ #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 @@ -1308,6 +1329,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()"); @@ -1328,6 +1383,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; } @@ -2318,7 +2376,7 @@ static int IdentifyCertKey(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; @@ -2364,7 +2422,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 @@ -2578,6 +2636,13 @@ 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; RefreshPublicKeyAlgo(ctx); @@ -2627,6 +2692,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 @@ -13494,6 +13566,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 { @@ -13870,6 +13945,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. */ @@ -13881,26 +13960,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) { @@ -14016,6 +14144,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, @@ -15216,6 +15383,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 @@ -15276,23 +15698,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; + } } } @@ -15301,8 +15741,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)); @@ -15374,20 +15829,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; + } } } @@ -15428,10 +15911,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; + } } } @@ -17713,6 +18204,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); @@ -17840,17 +18357,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"); + } } } @@ -18175,29 +18734,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) { @@ -18298,22 +18888,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 1f3f7cc49..3872df931 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 @@ -3299,6 +3310,374 @@ int wolfSSH_CTX_AddRootCert_file(WOLFSSH_CTX* ctx, const char* name) #endif /* !NO_FILESYSTEM && !WOLFSSH_USER_FILESYSTEM */ +#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 e1eff261c..a0afec029 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 @@ -12784,6 +12801,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 @@ -16701,6 +16890,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 c40d78edc..afc076658 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -60,6 +60,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 */ @@ -769,6 +778,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; @@ -1296,6 +1320,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); @@ -1577,6 +1602,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 dfb45ab60..a206b4368 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 @@ -551,6 +556,11 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, WOLFSSH_API int wolfSSH_CTX_AddRootCert_file(WOLFSSH_CTX* ctx, const char* name); #endif + #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 #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, word32 windowSz, word32 maxPacketSz); From e7aa41e7fd110ac7406dd616033b221c5cf6e55c Mon Sep 17 00:00:00 2001 From: JacobBarthelmeh Date: Mon, 3 Aug 2026 23:09:49 -0600 Subject: [PATCH 6/6] fix for flags handling, ocsp case, macro guards, unused variable, changed default from MY to required to be set enable SHA1 with windows cert store test case expand test cases, adjust to authorized key file, minor dead code adjustments add more documentation, refactor duplicate code sections, clean up test cases, more adjustments to logging spamming protections --- .github/workflows/windows-cert-store-test.yml | 941 +++++++++++++- README.md | 66 + apps/wolfsshd/auth.c | 211 +++- apps/wolfsshd/auth.h | 1 + apps/wolfsshd/configuration.c | 416 +++++-- apps/wolfsshd/configuration.h | 25 +- apps/wolfsshd/test/create_sshd_config.sh | 16 +- apps/wolfsshd/test/run_all_sshd_tests.sh | 58 +- apps/wolfsshd/test/test_configuration.c | 343 +++++- apps/wolfsshd/wolfsshd.c | 871 +++++++++++-- configure.ac | 31 +- examples/client/common.c | 136 ++- examples/client/common.h | 13 +- examples/echoserver/echoserver.c | 138 ++- examples/scpclient/scpclient.c | 3 +- examples/sftpclient/sftpclient.c | 309 +++-- ide/winvs/README.md | 5 + ide/winvs/api-test/api-test.vcxproj | 18 +- ide/winvs/client/client.vcxproj | 16 +- ide/winvs/echoserver/echoserver.vcxproj | 16 +- ide/winvs/testsuite/testsuite.vcxproj | 32 +- ide/winvs/unit-test/unit-test.vcxproj | 18 +- ide/winvs/user_settings.h | 22 + .../wolfsftp-client/wolfsftp-client.vcxproj | 16 +- ide/winvs/wolfssh/wolfssh.vcxproj | 8 +- ide/winvs/wolfsshd/wolfsshd.vcxproj | 22 +- src/certman.c | 285 ++++- src/internal.c | 1083 +++++++++++++---- src/ssh.c | 891 ++++++++++---- tests/api.c | 4 + tests/sftp.c | 55 + tests/unit.c | 476 +++++++- wolfssh/certman.h | 58 +- wolfssh/internal.h | 33 +- wolfssh/ssh.h | 42 + wolfssh/test.h | 53 +- 36 files changed, 5516 insertions(+), 1215 deletions(-) diff --git a/.github/workflows/windows-cert-store-test.yml b/.github/workflows/windows-cert-store-test.yml index eb4fcb84f..c5ca83e20 100644 --- a/.github/workflows/windows-cert-store-test.yml +++ b/.github/workflows/windows-cert-store-test.yml @@ -16,6 +16,11 @@ on: branches: [ 'master', 'main', 'release/**' ] pull_request: branches: [ '*' ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true env: WOLFSSL_SOLUTION_FILE_PATH: wolfssl64.sln @@ -32,6 +37,7 @@ env: jobs: build: runs-on: windows-latest + timeout-minutes: 30 steps: - uses: actions/checkout@v4 @@ -57,9 +63,30 @@ 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. - printf '\n/* Appended by windows-cert-store-test CI */\n#define WOLFSSH_WINDOWS_CERT_STORE\n' >> ${{env.USER_SETTINGS_H_NEW}} + # 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: + # 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. + 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 @@ -82,10 +109,40 @@ 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: | + # Non-zero native exits are handled in-script; without this, pwsh 7.4 + # can turn them into terminating errors before the handler runs. + $PSNativeCommandUseErrorActionPreference = $false + $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: @@ -98,6 +155,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 @@ -121,7 +179,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 @@ -132,37 +195,137 @@ 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: 20 + + 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 + + # Compile the cert-store sources with the mingw cross compiler so a + # GCC/mingw-only break in the new code (header ordering, format checks, + # older-SDK differences from MSVC) is caught, not just configure-tested. + # A cross-built wolfSSL library is not needed to compile these objects: + # only the wolfSSL headers are consumed, generated by configuring a + # wolfSSL source checkout for the same mingw host. + - name: mingw compile check of the cert-store sources + run: | + git clone --depth 1 https://github.com/wolfssl/wolfssl.git wolfssl-src + (cd wolfssl-src && ./autogen.sh > /dev/null && \ + ./configure --host=x86_64-w64-mingw32 --enable-ssh > /dev/null) + x86_64-w64-mingw32-gcc -fsyntax-only -Wall \ + -DWOLFSSH_CERTS -DWOLFSSH_WINDOWS_CERT_STORE -DWOLFSSH_SSHD \ + -DHAVE_CONFIG_H -I. -Iwolfssl-src \ + src/certman.c src/ssh.c src/internal.c \ + apps/wolfsshd/wolfsshd.c apps/wolfsshd/configuration.c \ + apps/wolfsshd/auth.c + + - 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 > 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 > 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 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 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: rsa + test_name: "Server-File-Client-Store-RSA" steps: - uses: actions/checkout@v4 @@ -194,6 +357,41 @@ jobs: # for x509 clients and imported into the store for store clients. cd keys bash renewcerts.sh testuser + # renewcerts.sh has no 'set -e' and exits 0 even when an openssl call + # failed, so verify the CA and server cert it silently regenerates + # and that the rest of the matrix depends on. + openssl x509 -in ca-cert-ecc.pem -noout + openssl verify -CAfile ca-cert-ecc.pem server-cert.pem + + # 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 + 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 + 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 + 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 .. if [[ ! -f "keys/testuser-cert.der" || ! -f "keys/testuser-key.der" ]]; then @@ -201,6 +399,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 @@ -208,6 +429,9 @@ jobs: working-directory: ${{ github.workspace }}\wolfssh shell: pwsh run: | + # Non-zero native exits (the openssl RSA-then-EC fallback) are + # handled in-script. + $PSNativeCommandUseErrorActionPreference = $false # Server host key: self-signed cert in LocalMachine\My so the # wolfsshd service (LocalSystem) can access it. if ("${{ matrix.server_key_source }}" -eq "store") { @@ -260,6 +484,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 @@ -301,9 +529,53 @@ 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: | + # Non-zero certutil exits are handled in-script. + $PSNativeCommandUseErrorActionPreference = $false + # 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 + } + + # A store holding only an end-entity certificate, for the negative + # test of the CertIsCA basicConstraints filter: with no CA:TRUE cert + # present the daemon must refuse to start rather than promote the + # leaf to a login authority. + $leafDer = (Resolve-Path "keys\testuser-cert.der").Path + certutil -addstore -f wolfSSHLeafOnlyCA $leafDer + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: certutil -addstore wolfSSHLeafOnlyCA"; exit 1 } + - name: Create Windows user testuser shell: pwsh run: | + # net user's "already exists" recovery below inspects $LASTEXITCODE; + # without this, pwsh 7.4 throws at the call site first. + $PSNativeCommandUseErrorActionPreference = $false $homeDir = "C:\Users\testuser" $sshDir = "$homeDir\.ssh" $authKeysFile = "$sshDir\authorized_keys" @@ -329,6 +601,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). @@ -347,17 +632,29 @@ 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 - # HostCertificate separately. + # The certificate is part of the store entry. HostKey and + # HostCertificate alongside HostKeyStore are rejected at startup. $configContent += @" HostKeyStore My @@ -428,27 +725,33 @@ 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 working-directory: ${{ github.workspace }}\wolfssh shell: pwsh run: | + # icacls failures are handled in-script via $LASTEXITCODE. + $PSNativeCommandUseErrorActionPreference = $false # 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 @@ -460,6 +763,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' @@ -472,7 +779,9 @@ jobs: $echoserverPath = $env:ECHOSERVER_PATH $exeDir = Split-Path -Parent $echoserverPath $port = ${{env.TEST_PORT}} - $spec = "My:wolfSSH-Test-Server:LOCAL_MACHINE" + # Reuse the exported CN rather than duplicating the constant the + # wolfsshd config path uses. + $spec = "My:$($env:SERVER_CERT_SUBJECT):LOCAL_MACHINE" $wolfsshRoot = "${{ github.workspace }}\wolfssh" # -a : verify client X.509 certs @@ -481,38 +790,62 @@ jobs: $clientCert = (Resolve-Path (Join-Path $wolfsshRoot $env:CLIENT_CERT_FILE)).Path $echoArgs = @("-W", $spec, "-p", $port, "-a", $caCertPem, "-K", "testuser:$clientCert") - $argStr = $echoArgs -join " " + # 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 + + # Quote every element so a path containing a space cannot split an + # argument when the array is flattened for cmd.exe. + $argStr = ($echoArgs | ForEach-Object { '"{0}"' -f $_ }) -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" + # Launch via a batch file: passing the quoted command line as one + # Start-Process argument nests quotes, which cmd mangles. + $batFile = Join-Path $wolfsshRoot "start_echoserver.bat" + Set-Content -Path $batFile -Encoding ASCII -Value @( + "@echo off", + "`"$echoserverPath`" $argStr > `"$echoLogFile`" 2>&1" + ) Start-Process -FilePath "cmd.exe" ` - -ArgumentList "/c", "start", "/B", "cmd", "/c", $cmdLine ` + -ArgumentList "/c", "start", "/B", "cmd", "/c", $batFile ` -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)" + + # The detached launch goes through two intermediate cmd.exe + # processes, so poll for the echoserver process instead of a fixed + # sleep, and only treat its absence as a crash once it has been seen + # running. + $seenRunning = $false + for ($i = 0; $i -lt 10 -and -not $seenRunning; $i++) { + $proc = Get-Process -Name "echoserver" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($proc) { + $seenRunning = $true + Add-Content -Path $env:GITHUB_ENV -Value "ECHOSERVER_PID=$($proc.Id)" + Write-Host "echoserver started with PID $($proc.Id)" + } else { + Start-Sleep -Seconds 1 + } } # 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)) { + if ($seenRunning -and + -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) { + if (-not $ready) { Write-Host "ERROR: Port $port not listening after ${timeout}s" if (Test-Path $echoLogFile) { Get-Content $echoLogFile } exit 1 @@ -523,7 +856,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 @@ -567,8 +904,75 @@ 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' + 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), which also means + # a fallback to a file-based host key could not pass this step. + $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" + 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 ` + -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 + } + 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 if: matrix.server_key_source == 'store' shell: pwsh @@ -576,17 +980,277 @@ 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 + # -ErrorAction Stop in a try/catch so a cmdlet failure (module or + # WMI hiccup) fails the step loudly instead of reading as "released". + while ($true) { + try { + $listening = Get-NetTCPConnection -LocalPort $port ` + -State Listen -ErrorAction Stop + } catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] { + # no matching connection: the port is released + $listening = $null + } + if (-not $listening) { break } + 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=" + - 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: | + # 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}} + + $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" ` + -NoNewWindow -PassThru + # Wait for the refusal instead of sleeping a fixed interval; a slow + # cold start on a loaded runner must not read as "still running". + $exited = $proc.WaitForExit(30000) + + $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 + + $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" + $failed = $true + } + if (-not $exited) { + Write-Host "ERROR: wolfsshd is still running with an empty user CA store" + $failed = $true + } + # -ErrorAction Stop in a try/catch so a cmdlet failure cannot be read + # as "not listening" and silently pass the assertion. + try { + $listening = Get-NetTCPConnection -LocalPort $port -State Listen ` + -ErrorAction Stop + } catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] { + $listening = $null + } + if ($listening) { + Write-Host "ERROR: wolfsshd is listening on port $port with an empty user CA store" + $failed = $true + } + if (-not $exited) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + if ($failed) { exit 1 } + Write-Host "wolfsshd refused to start with the empty user CA store" + + # Every fail-closed startup validation the cert-store options carry, each + # asserted on its specific error message, the process exiting, and no + # listener appearing. Runs once (the user_ca_source: store entry). + - name: wolfSSHd rejects invalid cert-store configurations + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 10 + run: | + $port = ${{env.TEST_PORT}} + $sshdPath = (Resolve-Path $env:SSHD_PATH).Path + $hostKey = (Resolve-Path "keys\server-key.pem").Path + $hostCert = (Resolve-Path "keys\server-cert.pem").Path + $caPem = (Resolve-Path "keys\ca-cert-ecc.pem").Path + $script:anyFailed = $false + + function Test-SshdRejects { + param($Desc, $ConfigLines, $Expect, $ExtraArgs = @()) + Write-Host "--- $Desc" + $cfg = "sshd_config_negative" + $ConfigLines | Out-File -FilePath $cfg -Encoding ASCII + $cfgFull = (Resolve-Path $cfg).Path + $sshdArgs = @("-D", "-d", "-f", $cfgFull, "-p", ${{env.TEST_PORT}}) + $sshdArgs += $ExtraArgs + $proc = Start-Process -FilePath $sshdPath -ArgumentList $sshdArgs ` + -RedirectStandardOutput "sshd_neg_out.txt" ` + -RedirectStandardError "sshd_neg_err.txt" ` + -NoNewWindow -PassThru + $exited = $proc.WaitForExit(30000) + $log = "" + foreach ($f in @("sshd_neg_out.txt", "sshd_neg_err.txt")) { + if (Test-Path $f) { $log += (Get-Content $f -Raw) } + } + $ok = $true + if ($log -notmatch $Expect) { + Write-Host "ERROR: expected '$Expect' in output" + Write-Host $log + $ok = $false + } + if (-not $exited) { + Write-Host "ERROR: wolfsshd is still running" + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + $ok = $false + } + try { + $listening = Get-NetTCPConnection -LocalPort ${{env.TEST_PORT}} ` + -State Listen -ErrorAction Stop + } catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException] { + $listening = $null + } + if ($listening) { + Write-Host "ERROR: wolfsshd is listening" + $ok = $false + } + if ($ok) { Write-Host "PASSED" } else { $script:anyFailed = $true } + } + + $base = @("Port $port", "HostKey $hostKey", "TrustedUserCAKeys $caPem") + + Test-SshdRejects "user CA store with no store name" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserDwFlags LOCAL_MACHINE") ` + "no store name" + Test-SshdRejects "user CA store with no store location" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara wolfSSHTestCA") ` + "no store location" + Test-SshdRejects "unsupported store provider" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara wolfSSHTestCA", + "wolfSSH_WinUserDwFlags LOCAL_MACHINE", + "wolfSSH_WinUserStores CERT_STORE_PROV_MEMORY") ` + "is not supported" + Test-SshdRejects "unrecognized store location" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara wolfSSHTestCA", + "wolfSSH_WinUserDwFlags NOT_A_LOCATION") ` + "Unrecognized user CA store flags" + foreach ($storeName in @("Root", "root", "Root\", "SID\Root")) { + Test-SshdRejects "OS trust store name '$storeName' refused" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara $storeName", + "wolfSSH_WinUserDwFlags LOCAL_MACHINE") ` + "names a Windows system" + } + Test-SshdRejects "WinUser options without the store enabled" ` + ($base + @("wolfSSH_WinUserPvPara wolfSSHTestCA")) ` + "wolfSSH_TrustedUserCAStore is not enabled" + Test-SshdRejects "user CA store with only a leaf certificate" ` + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara wolfSSHLeafOnlyCA", + "wolfSSH_WinUserDwFlags LOCAL_MACHINE") ` + "No usable CA certificates found in store" + Test-SshdRejects "HostKeyStore without HostKeyStoreSubject" ` + ($base[0..0] + @("TrustedUserCAKeys $caPem", "HostKeyStore My", + "HostKeyStoreFlags LOCAL_MACHINE")) ` + "HostKeyStoreSubject is missing" + Test-SshdRejects "HostKeyStore without HostKeyStoreFlags" ` + ($base[0..0] + @("TrustedUserCAKeys $caPem", "HostKeyStore My", + "HostKeyStoreSubject wolfSSH-Test-Server")) ` + "HostKeyStoreFlags is missing" + Test-SshdRejects "HostKeyStoreSubject/Flags without HostKeyStore" ` + ($base[0..0] + @("TrustedUserCAKeys $caPem", + "HostKeyStoreSubject wolfSSH-Test-Server", + "HostKeyStoreFlags LOCAL_MACHINE")) ` + "HostKeyStore is missing" + Test-SshdRejects "HostKeyStore conflicts with HostKey" ` + ($base + @("HostKeyStore My", + "HostKeyStoreSubject wolfSSH-Test-Server", + "HostKeyStoreFlags LOCAL_MACHINE")) ` + "HostKey conflicts" + Test-SshdRejects "HostKeyStore conflicts with HostCertificate" ` + ($base[0..0] + @("TrustedUserCAKeys $caPem", + "HostCertificate $hostCert", "HostKeyStore My", + "HostKeyStoreSubject wolfSSH-Test-Server", + "HostKeyStoreFlags LOCAL_MACHINE")) ` + "HostCertificate conflicts" + Test-SshdRejects "-h conflicts with HostKeyStore" ` + ($base[0..0] + @("TrustedUserCAKeys $caPem", "HostKeyStore My", + "HostKeyStoreSubject wolfSSH-Test-Server", + "HostKeyStoreFlags LOCAL_MACHINE")) ` + "-h host key file conflicts" @("-h", $hostKey) + # This build has no WOLFSSL_SYS_CA_CERTS, so the system CA directive + # must fail closed rather than run without the configured anchors. + Test-SshdRejects "system CA on a build without WOLFSSL_SYS_CA_CERTS" ` + ($base + @("wolfSSH_TrustedSystemCAKeys yes")) ` + "WOLFSSL_SYS_CA_CERTS" + + if ($script:anyFailed) { exit 1 } + Write-Host "All invalid cert-store configurations were rejected" + + # The store hive warning must reach the log without -d: the log callback + # writes WS_LOG_WARN unconditionally. CurrentUser has no wolfSSHTestCA + # store, so the daemon exits after warning and nothing is left running. + - name: wolfSSHd logs store hive warning without -d + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 3 + run: | + $port = ${{env.TEST_PORT}} + $hostKey = (Resolve-Path "keys\server-key.pem").Path + @("Port $port", "HostKey $hostKey", + "wolfSSH_TrustedUserCAStore yes", + "wolfSSH_WinUserPvPara wolfSSHTestCA", + "wolfSSH_WinUserDwFlags CURRENT_USER") | + Out-File -FilePath sshd_config_warn -Encoding ASCII + $cfgFull = (Resolve-Path "sshd_config_warn").Path + $logFile = Join-Path (Get-Location).Path "sshd_warn_log.txt" + $proc = Start-Process -FilePath (Resolve-Path $env:SSHD_PATH).Path ` + -ArgumentList @("-D", "-f", $cfgFull, "-p", $port, "-E", $logFile) ` + -NoNewWindow -PassThru + $exited = $proc.WaitForExit(30000) + if (-not $exited) { + Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue + } + $log = "" + if (Test-Path $logFile) { $log = Get-Content $logFile -Raw } + Write-Host "=== wolfsshd log ===" + Write-Host $log + if ($log -notmatch "without elevation") { + Write-Host "ERROR: hive warning did not reach the log without -d" + exit 1 + } + Write-Host "Hive warning was logged without -d" + - name: Start wolfSSHd as Windows service working-directory: ${{ github.workspace }}\wolfssh shell: pwsh run: | + # sc.exe failures are diagnosed in-script (query + event log dump); + # without this, pwsh 7.4 throws before the diagnostics run. + $PSNativeCommandUseErrorActionPreference = $false $sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path $configPathFull = (Resolve-Path "sshd_config_test").Path $serviceName = "wolfsshd" @@ -605,6 +1269,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 @@ -639,6 +1306,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 @@ -696,8 +1364,199 @@ 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" + # The certificate identity binding: a client certificate whose identity + # does not match the requested account must be rejected, and the match is + # case-insensitive like Windows account names. This build defines + # OPENSSL_ALL, which turns on WOLFSSL_ASN_ALL and with it WOLFSSL_FPKI in + # wolfSSL's settings.h, so the binding evaluated here is the certificate + # UPN (renewcerts.cnf gives the test certs an msUPN altname); the + # subject-CN branch only compiles on non-FPKI wolfSSL builds and so has + # no runtime coverage here -- exercising it needs a matrix entry whose + # wolfSSL user_settings.h omits OPENSSL_ALL. Runs + # against the already-running service on the user_ca_source: store + # entry, whose config sets no AuthorizedKeysFile so the identity binding + # is what decides. + - name: Client certificate UPN binding is enforced (FPKI build) + if: matrix.user_ca_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 5 + run: | + $PSNativeCommandUseErrorActionPreference = $false + $testPort = ${{env.TEST_PORT}} + $sftpPath = $env:SFTP_PATH + $caCertDer = (Resolve-Path "keys\ca-cert-ecc.der").Path + + # Issue a certificate for a CN that names no requested account, + # signed by the same trusted CA. Disable MSYS path conversion so Git + # Bash does not rewrite the leading-slash -subj argument into + # C:/Program Files/Git/C=US/... + $env:MSYS_NO_PATHCONV = "1" + $env:MSYS2_ARG_CONV_EXCL = "*" + Push-Location keys + & bash -c "touch index.txt && sed 's/fred/wronguser/g' renewcerts.cnf > renewcerts-wronguser.cnf && openssl ecparam -name prime256v1 -genkey -noout -out wronguser-key.pem && openssl req -subj '/C=US/ST=WA/L=Seattle/O=wolfSSL Inc/OU=Development/CN=wronguser' -key wronguser-key.pem -out wronguser-cert.csr -config renewcerts-wronguser.cnf -new -nodes && openssl x509 -req -in wronguser-cert.csr -days 3650 -extfile renewcerts-wronguser.cnf -extensions v3_wronguser -CA ca-cert-ecc.pem -CAkey ca-key-ecc.pem -out wronguser-cert.pem -set_serial 8 && openssl x509 -in wronguser-cert.pem -outform DER -out wronguser-cert.der && openssl ec -in wronguser-key.pem -outform DER -out wronguser-key.der" + if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Host "ERROR: wronguser cert creation failed"; exit 1 } + Pop-Location + + # Recreate the service with debug logging so a rejection in this step + # is diagnosable: the service logs only to OutputDebugString + # otherwise. C:\Windows\Temp is writable by LocalSystem where the + # workspace is not. + $cnLog = "C:\Windows\Temp\wolfsshd_cn_debug.log" + $sshdPathFull = (Resolve-Path $env:SSHD_PATH).Path + $configPathFull = (Resolve-Path "sshd_config_test").Path + Stop-Service -Name wolfsshd -Force + $binPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p $testPort -d -E `"$cnLog`"" + sc.exe config wolfsshd binPath= $binPath | Out-Null + if ($LASTEXITCODE -ne 0) { Write-Host "ERROR: sc config failed"; exit 1 } + Start-Service -Name wolfsshd + Start-Sleep -Seconds 3 + $svc = Get-Service -Name wolfsshd + if ($svc.Status -ne 'Running') { + Write-Host "ERROR: service not running after reconfigure" + sc.exe query wolfsshd + exit 1 + } + + "quit" | Out-File -FilePath sftp_cn_commands.txt -Encoding ASCII + + # Run all three connections first, then stop the service and evaluate: + # the service keeps the -E log open without read sharing, so the log + # is only readable once the service has been stopped. + + # Control connection: the exact-case user against the reconfigured + # service must still succeed, so a later failure is attributable to + # the case-differing name rather than to service state. + $args0 = @("-u", "testuser", "-h", "localhost", "-p", "$testPort", + "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path, + "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path, + "-A", $caCertDer, "-X") + $p = Start-Process -FilePath $sftpPath -ArgumentList $args0 ` + -RedirectStandardInput "sftp_cn_commands.txt" ` + -RedirectStandardOutput "sftp_cn0_out.txt" ` + -RedirectStandardError "sftp_cn0_err.txt" ` + -Wait -NoNewWindow -PassThru + $controlExit = $p.ExitCode + + # CN=wronguser presented for -u testuser must fail + $args1 = @("-u", "testuser", "-h", "localhost", "-p", "$testPort", + "-J", (Resolve-Path "keys\wronguser-cert.der").Path, + "-i", (Resolve-Path "keys\wronguser-key.der").Path, + "-A", $caCertDer, "-X") + $p = Start-Process -FilePath $sftpPath -ArgumentList $args1 ` + -RedirectStandardInput "sftp_cn_commands.txt" ` + -RedirectStandardOutput "sftp_cn_out.txt" ` + -RedirectStandardError "sftp_cn_err.txt" ` + -Wait -NoNewWindow -PassThru + $wrongExit = $p.ExitCode + + # CN=testuser presented for -u TESTUSER must succeed: Windows account + # names are case-insensitive and the CN match follows suit + $args2 = @("-u", "TESTUSER", "-h", "localhost", "-p", "$testPort", + "-J", (Resolve-Path $env:CLIENT_CERT_FILE).Path, + "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path, + "-A", $caCertDer, "-X") + $p = Start-Process -FilePath $sftpPath -ArgumentList $args2 ` + -RedirectStandardInput "sftp_cn_commands.txt" ` + -RedirectStandardOutput "sftp_cn2_out.txt" ` + -RedirectStandardError "sftp_cn2_err.txt" ` + -Wait -NoNewWindow -PassThru + $upperExit = $p.ExitCode + + # Release the log before reading it + Stop-Service -Name wolfsshd -Force -ErrorAction SilentlyContinue + $srvLog = "" + if (Test-Path $cnLog) { $srvLog = Get-Content $cnLog -Raw } + + $failed = $false + if ($controlExit -ne 0) { + Write-Host "ERROR: exact-case control connection failed" + Get-Content sftp_cn0_out.txt, sftp_cn0_err.txt + $failed = $true + } else { + Write-Host "Exact-case control connection accepted" + } + if ($wrongExit -eq 0) { + Write-Host "ERROR: CN=wronguser was accepted for user testuser" + Get-Content sftp_cn_out.txt, sftp_cn_err.txt + $failed = $true + } + elseif ($srvLog -notmatch "incorrect user cert") { + # The rejection must be the CN identity check, not an incidental + # failure earlier or later in the exchange. + Write-Host "ERROR: wronguser was rejected for a reason other than the CN check" + Get-Content sftp_cn_out.txt, sftp_cn_err.txt + $failed = $true + } else { + Write-Host "CN mismatch rejected (exit $wrongExit)" + } + if ($upperExit -ne 0) { + Write-Host "ERROR: case-differing user TESTUSER was rejected" + Get-Content sftp_cn2_out.txt, sftp_cn2_err.txt + $failed = $true + } else { + Write-Host "Case-insensitive CN match accepted" + } + + if ($failed) { + if ($srvLog -ne "") { + Write-Host "=== wolfsshd debug log (tail) ===" + $lines = $srvLog -split "`n" + $lines | Select-Object -Last 400 + } else { + Write-Host "(no wolfsshd debug log was written)" + } + exit 1 + } + + # Restore the original binPath and restart the service so any step + # added after this one gets a running, normally-configured daemon. + $origBinPath = "`"$sshdPathFull`" -f `"$configPathFull`" -p $testPort" + sc.exe config wolfsshd binPath= $origBinPath | Out-Null + Start-Service -Name wolfsshd + + # -W supplies both keys, so combining it with -i/-j/-J is a usage error + # the client must refuse before connecting. + - name: SFTP client rejects -W combined with -i + if: matrix.client_key_source == 'store' + working-directory: ${{ github.workspace }}\wolfssh + shell: pwsh + timeout-minutes: 2 + run: | + $PSNativeCommandUseErrorActionPreference = $false + "quit" | Out-File -FilePath sftp_wconflict_cmd.txt -Encoding ASCII + $conflictArgs = @("-u", "testuser", "-h", "localhost", + "-p", "${{env.TEST_PORT}}", + "-W", "My:$($env:CLIENT_CERT_SUBJECT):CURRENT_USER", + "-i", (Resolve-Path $env:CLIENT_KEY_FILE).Path) + $p = Start-Process -FilePath $env:SFTP_PATH -ArgumentList $conflictArgs ` + -RedirectStandardInput "sftp_wconflict_cmd.txt" ` + -RedirectStandardOutput "sftp_wconflict_out.txt" ` + -RedirectStandardError "sftp_wconflict_err.txt" ` + -Wait -NoNewWindow -PassThru + $log = "" + foreach ($f in @("sftp_wconflict_out.txt", "sftp_wconflict_err.txt")) { + if (Test-Path $f) { $log += (Get-Content $f -Raw) } + } + if ($p.ExitCode -eq 0) { + Write-Host "ERROR: -W with -i was accepted" + exit 1 + } + if ($log -notmatch "can not be used with") { + Write-Host "ERROR: expected the -W conflict message" + Write-Host $log + exit 1 + } + Write-Host "-W with -i rejected as expected" + - name: Cleanup if: always() shell: pwsh @@ -728,4 +1587,20 @@ jobs: Get-ChildItem -Path "Cert:\LocalMachine\My" | Where-Object { $_.Subject -like "*wolfSSH-Test*" } | Remove-Item -Force -ErrorAction SilentlyContinue + foreach ($s in @("wolfSSHTestCA", "wolfSSHEmptyCA", "wolfSSHLeafOnlyCA")) { + Get-ChildItem -Path "Cert:\LocalMachine\$s" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + # Remove the store itself (a registry key), not just its + # contents, so nothing persists across runs on a self-hosted + # runner + Remove-Item -Path "HKLM:\SOFTWARE\Microsoft\SystemCertificates\$s" ` + -Recurse -Force -ErrorAction SilentlyContinue + } + + # Remove generated key material from the checkout (private keys for + # CA-signed identities must not persist on a self-hosted runner) + Remove-Item -Path "$env:GITHUB_WORKSPACE\wolfssh\keys\wronguser-*" ` + -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$env:GITHUB_WORKSPACE\wolfssh\keys\testuser-key.*" ` + -Force -ErrorAction SilentlyContinue Write-Host "Cleaned up test certificates" diff --git a/README.md b/README.md index 98fe06667..2535049d5 100644 --- a/README.md +++ b/README.md @@ -538,6 +538,72 @@ fred-cert.der would be: $ ./examples/client/client -u fred -J ./keys/fred-cert.der -i ./keys/fred-key.der +WINDOWS CERTIFICATE STORE +========================= + +On Windows, host and user keys can come from the MS Certificate Store instead +of files. Requires certificate support (`--enable-certs` or `WOLFSSH_CERTS`); +enable it with the `--enable-windows-cert-store` build option (mingw hosts +only) or by defining `WOLFSSH_WINDOWS_CERT_STORE`. The build links against +`crypt32` and `ncrypt`. For the Visual Studio build see the comment block in +`ide/winvs/user_settings.h`, including the `WOLFSSH_NO_SHA1_SOFT_DISABLE` and +`WC_SIG_MIN_HASH_TYPE` caveats an RSA store certificate needs (RFC 6187's only +RSA algorithm, `x509v3-ssh-rsa`, signs with SHA-1); ECDSA store keys need +neither. + +The echoserver and the SFTP client take a `-W store:subject[:flags]` option +naming the store, the certificate's subject CN, and optionally the store +location. Accepted location names are CURRENT_USER (the default), +LOCAL_MACHINE, USERS, CURRENT_SERVICE, SERVICES, CURRENT_USER_GROUP_POLICY, +LOCAL_MACHINE_GROUP_POLICY and LOCAL_MACHINE_ENTERPRISE, each also accepted +with a `CERT_SYSTEM_STORE_` prefix or as a number. `-W` supplies both the +certificate and its private key; in the SFTP client it therefore cannot be +combined with `-i`, `-j`, or `-J` (the echoserver's options of those names are +unrelated and remain usable). `-W` also skips the wolfssh home directory +search so file arguments resolve against the current directory. + + $ ./examples/echoserver/echoserver -W "My:wolfSSH-Server:LOCAL_MACHINE" -a ./keys/ca-cert-ecc.pem + + $ ./examples/sftpclient/wolfsftp -u testuser -W "My:testuser:CURRENT_USER" -A ./keys/ca-cert-ecc.der -X + +wolfSSHd gains these configuration directives, all global only (they are +rejected inside a `Match` block): + +* `HostKeyStore `, `HostKeyStoreSubject `, and + `HostKeyStoreFlags ` select the host key from a certificate + store. All three must be set together, and they conflict with `HostKey`, + `HostCertificate`, and the `-h` command line option. +* `wolfSSH_TrustedUserCAStore yes|no` loads the client-certificate trust + anchors from a Windows store named by `wolfSSH_WinUserPvPara ` at + the mandatory location `wolfSSH_WinUserDwFlags ` + (`wolfSSH_WinUserStores` optionally names the provider; only + `CERT_STORE_PROV_SYSTEM` is supported). Only certificates with + basicConstraints CA:TRUE are loaded, and the OS-managed public trust + stores (`Root`, `AuthRoot`, `CA`, ...) are refused: every CA in the named + store becomes an SSH login authority, so point it at a store created for + this purpose that holds nothing but your own CA. +* `wolfSSH_TrustedSystemCAKeys yes|no` imports the OS trust store via + wolfSSL (`WOLFSSL_SYS_CA_CERTS`) as the client-certificate trust anchors. + On CN-binding builds (no FPKI) this additionally requires a per-user + `AuthorizedKeysFile` on every config node, so a subject CN match alone can + never log in. On FPKI builds every config node must set + `AuthorizedUPNDomains` or a per-user `AuthorizedKeysFile`; note + `AuthorizedUPNDomains` constrains only the certificate's UPN realm, not + which trusted CA issued it, so use it only when the OS trust store holds + solely your organization's CA. + +Note that the pre-existing `HostKey` and `HostCertificate` directives are now +also rejected when they appear after a `Match` block (matching OpenSSH); they +were previously accepted there and silently ignored, so a config that relied +on that will now stop the daemon at startup with a parse error. Builds made +with `WOLFSSH_IGNORE_UNKNOWN_CONFIG` instead log a warning and ignore the +directive, preserving the old behavior as a migration path. + +Without FPKI, a client certificate is bound to the requested account by a +case-insensitive subject CN match only; keep the trusted CA set narrow. Note +also that the config parser requires whitespace between an option name and +its value; the OpenSSH `Keyword=value` form is rejected. + TPM PUBLIC KEY AUTHENTICATION ============================= diff --git a/apps/wolfsshd/auth.c b/apps/wolfsshd/auth.c index a7429b2ad..88666442c 100644 --- a/apps/wolfsshd/auth.c +++ b/apps/wolfsshd/auth.c @@ -55,7 +55,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 @@ -998,6 +998,78 @@ static int IsAbsoluteAuthKeysPath(const char* path) return ret; } +#if defined(WOLFSSH_CERTS) || defined(WOLFSSHD_UNIT_TEST) +/* True when the AuthorizedKeysFile pattern resolves 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. Caveat: accounts that share a home directory + * (or Windows profile path) also share the file a relative pattern resolves + * to, so the per-user property holds only when home directories are + * distinct. */ +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; + } + + 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 || WOLFSSHD_UNIT_TEST */ + +/* Exported predicate answering "does this AuthorizedKeysFile pattern resolve + * to a distinct file per account". Compiled for every certificate-capable + * build so config-time gates (e.g. the FPKI wolfSSH_TrustedSystemCAKeys + * check in SetupCTX) can rely on it; without certificate support it always + * returns 0. */ +int wolfSSHD_AuthKeysPatternIsPerUser(const char* pattern) +{ +#if defined(WOLFSSH_CERTS) || defined(WOLFSSHD_UNIT_TEST) + 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. */ @@ -1836,12 +1908,14 @@ static int CheckPublicKeyUnix(const char* name, #ifdef _WIN32 +/* lower-case header names so mingw cross-builds resolve them on + * case-sensitive filesystems */ #include -#include -#include +#include +#include -#include -#include +#include +#include /* Pulled in from Advapi32.dll */ extern BOOL WINAPI LogonUserExExW(LPTSTR usr, @@ -1884,7 +1958,8 @@ static int _GetHomeDirectory(WOLFSSHD_AUTH* auth, const char* usr, WCHAR* out, i pInfo.lpUserName = usrW; if (LoadUserProfileW(wolfSSHD_GetAuthToken(auth), &pInfo) != TRUE) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Error %d loading user %s", GetLastError(), usr); + "[SSHD] Error %lu loading user %s", + (unsigned long)GetLastError(), usr); ret = WS_FATAL_ERROR; } @@ -1948,7 +2023,8 @@ static int CheckPasswordWIN(const char* usr, const byte* pw, word32 pwSz, WOLFSS } if (ret == WSSHD_AUTH_SUCCESS) { - pwWSz = MultiByteToWideChar(CP_UTF8, 0, pw, pwSz, NULL, 0); + pwWSz = MultiByteToWideChar(CP_UTF8, 0, (const char*)pw, pwSz, NULL, + 0); if (pwWSz <= 0) { ret = WSSHD_AUTH_FAILURE; } @@ -1962,7 +2038,8 @@ static int CheckPasswordWIN(const char* usr, const byte* pw, word32 pwSz, WOLFSS } if (ret == WSSHD_AUTH_SUCCESS) { - if (MultiByteToWideChar(CP_UTF8, 0, pw, pwSz, pwW, pwWSz) != pwWSz) { + if (MultiByteToWideChar(CP_UTF8, 0, (const char*)pw, pwSz, pwW, pwWSz) + != pwWSz) { ret = WSSHD_AUTH_FAILURE; } else { @@ -1973,8 +2050,8 @@ static int CheckPasswordWIN(const char* usr, const byte* pw, word32 pwSz, WOLFSS if (ret == WSSHD_AUTH_SUCCESS) { if (LogonUserExExW(usrW, dmW, pwW, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, NULL, &authCtx->token, NULL, NULL, NULL, NULL) != TRUE) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Windows failed with error %d when login in as user %s, " - "bad username or password", GetLastError(), usr); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Windows failed with error %lu when login in as user %s, " + "bad username or password", (unsigned long)GetLastError(), usr); wolfSSH_Log(WS_LOG_INFO, "[SSHD] Check user is allowed to 'Log on as batch job'"); ret = WSSHD_AUTH_FAILURE; } @@ -2051,7 +2128,8 @@ static int SetupUserTokenWin(const char* usr, if ((rc = LsaRegisterLogonProcess(&processName, &lsaHandle, &oMode)) != STATUS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] LSA Register Logon Process Error %d", LsaNtStatusToWinError(rc)); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] LSA Register Logon Process Error %lu", + (unsigned long)LsaNtStatusToWinError(rc)); ret = WSSHD_AUTH_FAILURE; } } @@ -2064,7 +2142,8 @@ static int SetupUserTokenWin(const char* usr, authName.Length = (USHORT)WSTRLEN(MSV1_0_PACKAGE_NAME); authName.MaximumLength = authName.Length + 1; if ((rc = LsaLookupAuthenticationPackage(lsaHandle, &authName, &authId)) != STATUS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] LSA Lookup Authentication Package Error %d", rc); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] LSA Lookup Authentication Package Error %lu", + (unsigned long)rc); ret = WSSHD_AUTH_FAILURE; } } @@ -2111,7 +2190,7 @@ static int SetupUserTokenWin(const char* usr, NTSTATUS subStatus; QUOTA_LIMITS quotas; DWORD profileSz; - PKERB_INTERACTIVE_PROFILE profile = NULL; + PVOID profile = NULL; LUID logonId = { 0, 0 }; WMEMSET(&originName, 0, sizeof(LSA_STRING)); @@ -2120,8 +2199,8 @@ static int SetupUserTokenWin(const char* usr, originName.MaximumLength = originName.Length + 1; if ((rc = LsaLogonUser(lsaHandle, &originName, Network, authId, authInfo, authInfoSz, NULL, &sourceContext, &profile, &profileSz, &logonId, &authCtx->token, "as, &subStatus)) != STATUS_SUCCESS) { - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Windows failed with status %X, SubStatus %d, when login in as user %s", - rc, subStatus, usr); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Windows failed with status %lX, SubStatus %ld, when login in as user %s", + (unsigned long)rc, (long)subStatus, usr); ret = WSSHD_AUTH_FAILURE; } @@ -2311,7 +2390,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) { @@ -2331,8 +2411,16 @@ WOLFSSHD_STATIC int MatchUPNToUser(const char* usr, const char* name, } } - /* the local part must equal the requested user name exactly */ - if ((int)XSTRLEN(usr) == idx && XSTRNCMP(usr, name, idx) == 0) { + /* The local part must equal the requested user name: exactly on + * Unix, case-insensitively on Windows where account names are + * case-insensitive. */ + if ((int)XSTRLEN(usr) == idx && +#ifdef _WIN32 + WSTRNCASECMP(usr, name, (size_t)idx) == 0 +#else + XSTRNCMP(usr, name, idx) == 0 +#endif + ) { if (allowList == NULL || *allowList == '\0') { /* no allowlist configured: keep local-part-only matching */ ret = 1; @@ -2369,7 +2457,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 */ #ifdef WOLFSSHD_UNIT_TEST @@ -2512,9 +2600,12 @@ static int RequestAuthentication(WS_UserAuthData* authData, usrConf = wolfSSHD_AuthGetUserConf(authCtx, usr, NULL, NULL, NULL, NULL, NULL); if (usrConf == NULL) { + /* bound the untrusted name so it cannot consume the whole + * fixed-width log message (control bytes are scrubbed by + * wolfSSH_Log itself) */ wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Failure to get user configuration for auth (user=%s)", - usr); + "[SSHD] Failure to get user configuration for auth " + "(user=%.32s)", usr); ret = WOLFSSH_USERAUTH_FAILURE; needFakeCheck = 1; } @@ -2599,11 +2690,19 @@ 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. */ + /* Bind the certificate to the requested user name via UPN with FPKI + * or CN without FPKI. The check always runs, even when a per-user + * AuthorizedKeysFile is configured: an exact certificate match + * against that file (checked below) proves the cert is authorized + * for the account, but it is not a substitute for the identity + * check here, so both are required as defense in depth. Note the + * strength of any AuthorizedKeysFile binding also rests on the + * file's integrity: on Windows, wolfSSHD_OpenSecureFile() performs + * no ownership or ACL checks, so that guarantee is only as good as + * the NTFS ACLs on the profile directory. */ if (authData->sf.publicKey.isCert) { #ifdef WOLFSSH_SMALL_STACK DecodedCert* dCert; @@ -2631,7 +2730,6 @@ static int RequestAuthentication(WS_UserAuthData* authData, else { int usrMatch = 0; #ifdef WOLFSSL_FPKI - int upnRealmUnchecked = 0; DNS_entry* current = dCert->altNames; const char* upnDomains = wolfSSHD_ConfigGetAuthorizedUPNDomains(usrConf); @@ -2641,37 +2739,44 @@ static int RequestAuthentication(WS_UserAuthData* authData, current->oidSum == UPN_OID) { /* bind the cert identity to the requested user; * MatchUPNToUser also enforces the realm allowlist - * when AuthorizedUPNDomains is set */ + * when AuthorizedUPNDomains is set. An unset + * allowlist is noticed once at startup from + * SetupCTX(), not here: this path is + * peer-triggered and the log callback writes + * WARN unconditionally. */ if (MatchUPNToUser(usr, current->name, current->len, upnDomains)) { usrMatch = 1; - if (upnDomains == NULL || *upnDomains == '\0') { - upnRealmUnchecked = 1; - } } } current = current->next; } - - /* a UPN matched but no realm policy is set; warn per auth - * attempt so the opt-in gap is visible, no shared state */ - if (upnRealmUnchecked) { - 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 && + /* 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. + * + * 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 && - XSTRNCMP(usr, dCert->subjectCN, + WSTRNCASECMP(usr, dCert->subjectCN, (size_t)dCert->subjectCNLen) == 0) { + /* CN-only binding (no issuer constraint) is a fixed + * build property, noticed once at startup from + * SetupCTX(); a per-attempt WARN here would let a + * peer grow the log with every attempt now that the + * log callback writes WARN without -d */ usrMatch = 1; } #endif if (usrMatch == 0) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] incorrect user cert " - "sent"); + "sent; certificate identity does not match the " + "requested user (user=%.32s)", usr); ret = WOLFSSH_USERAUTH_INVALID_PUBLICKEY; } } @@ -2700,14 +2805,16 @@ static int RequestAuthentication(WS_UserAuthData* authData, wolfSSHD_ConfigGetUserCAKeysFile(usrConf))) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Per-user TrustedUserCAKeys override is not enforced " - "for certificate authentication; rejecting (user=%s)", usr); + "for certificate authentication; rejecting (user=%.32s)", + usr); 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, @@ -2721,7 +2828,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. */ @@ -2729,14 +2836,15 @@ 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 " - "(user=%s)", usr); + "(user=%.32s)", usr); ret = WOLFSSH_USERAUTH_REJECTED; #endif } @@ -3185,6 +3293,7 @@ int wolfSSHD_AuthReducePermissions(WOLFSSHD_AUTH* auth) } flag = wolfSSHD_ConfigGetPrivilegeSeparation(auth->conf); + WOLFSSH_UNUSED(flag); #ifndef _WIN32 if (flag == WOLFSSHD_PRIV_SEPARAT || flag == WOLFSSHD_PRIV_SANDBOX) { wolfSSH_Log(WS_LOG_INFO, "[SSHD] Lowering permissions level"); diff --git a/apps/wolfsshd/auth.h b/apps/wolfsshd/auth.h index 637cdda7b..836249135 100644 --- a/apps/wolfsshd/auth.h +++ b/apps/wolfsshd/auth.h @@ -85,6 +85,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 4fd4155c4..5907d3c46 100644 --- a/apps/wolfsshd/configuration.c +++ b/apps/wolfsshd/configuration.c @@ -87,11 +87,11 @@ struct WOLFSSHD_CONFIG { char* hostKeyFile; char* hostCertFile; char* userCAKeysFile; -#ifdef WOLFSSH_WINDOWS_CERT_STORE +#ifdef WOLFSSHD_WIN_STORE_CONFIG char* hostKeyStore; char* hostKeyStoreSubject; char* hostKeyStoreFlags; -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ char* hostKeyAlgos; char* kekAlgos; char* listenAddress; @@ -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 WOLFSSHD_WIN_STORE_CONFIG char* winUserStores; char* winUserDwFlags; char* winUserPvPara; -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ WOLFSSHD_CONFIG* next; /* next config in list */ WOLFSSHD_CONFIG* head; /* global config the Match nodes branch from */ long loginTimer; @@ -193,7 +193,7 @@ static int CreateString(char** out, const char* in, int inSz, void* heap) } /* remove leading white spaces */ - while (idx < inSz && in[idx] == ' ') idx++; + while (idx < inSz && (in[idx] == ' ' || in[idx] == '\t')) idx++; if (idx == inSz) { ret = WS_BAD_ARGUMENT; @@ -201,7 +201,8 @@ static int CreateString(char** out, const char* in, int inSz, void* heap) if (ret == WS_SUCCESS) { for (tail = inSz - 1; tail > idx; tail--) { - if (in[tail] != '\n' && in[tail] != ' ' && in[tail] != '\r') { + if (in[tail] != '\n' && in[tail] != ' ' && in[tail] != '\r' && + in[tail] != '\t') { break; } } @@ -353,6 +354,46 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->heap); } +#ifdef WOLFSSHD_WIN_STORE_CONFIG + 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 /* WOLFSSHD_WIN_STORE_CONFIG */ + if (ret == WS_SUCCESS) { newConf->loginTimer = conf->loginTimer; newConf->port = conf->port; @@ -364,6 +405,8 @@ static WOLFSSHD_CONFIG* wolfSSHD_ConfigCopy(WOLFSSHD_CONFIG* conf) newConf->authKeysFileSet = conf->authKeysFileSet; newConf->strictModes = conf->strictModes; newConf->head = conf->head; + newConf->useSystemCA = conf->useSystemCA; + newConf->useUserCAStore = conf->useUserCAStore; } else { wolfSSHD_ConfigFree(newConf); @@ -400,16 +443,14 @@ void wolfSSHD_ConfigFree(WOLFSSHD_CONFIG* conf) FreeString(¤t->authorizedUPNDomains, heap); FreeString(¤t->usrAppliesTo, heap); FreeString(¤t->groupAppliesTo, heap); -#ifdef WOLFSSH_WINDOWS_CERT_STORE +#ifdef WOLFSSHD_WIN_STORE_CONFIG 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 /* WOLFSSHD_WIN_STORE_CONFIG */ WFREE(current, heap, DYNTYPE_SSHD); current = next; @@ -436,9 +477,6 @@ 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, @@ -455,12 +493,13 @@ 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 + OPT_AUTHORIZED_UPN_DOMAINS = 31, + OPT_HOST_KEY_STORE = 32, + OPT_HOST_KEY_STORE_SUBJECT = 33, + OPT_HOST_KEY_STORE_FLAGS = 34 }; static const CONFIG_OPTION options[] = { {OPT_AUTH_KEYS_FILE, "AuthorizedKeysFile"}, @@ -474,11 +513,13 @@ static const CONFIG_OPTION 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. */ + /* The parser requires a whitespace delimiter after the option name, which + * is the primary defence against a shorter name prefix-matching a longer + * one. As belt-and-braces, longer option names that share a common prefix + * MUST still appear before the shorter one: + * HostKeyStoreSubject/HostKeyStoreFlags before HostKeyStore, and all + * HostKeyStore* before HostKey. Kept unconditional so "HostKeyStore" + * never matches "HostKey" on non-store builds. */ {OPT_HOST_KEY_STORE_SUBJECT, "HostKeyStoreSubject"}, {OPT_HOST_KEY_STORE_FLAGS, "HostKeyStoreFlags"}, {OPT_HOST_KEY_STORE, "HostKeyStore"}, @@ -499,15 +540,44 @@ 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))) +#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) { @@ -785,7 +855,7 @@ static int HandleInclude(WOLFSSHD_CONFIG *conf, const char *value, int depth) /* Ignore trailing whitespace */ ptr = value + WSTRLEN(value) - 1; while (ptr != value) { - if (WISSPACE(*ptr)) { + if (WISSPACE((unsigned char)*ptr)) { ptr--; } else { @@ -1271,6 +1341,26 @@ 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) +{ + int ret = WS_SUCCESS; + + if (conf == NULL) { + ret = WS_BAD_ARGUMENT; + } + else if (conf->usrAppliesTo != NULL || conf->groupAppliesTo != NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Option %s is not supported inside a Match block", option); + ret = WS_BAD_ARGUMENT; + } + + return ret; +} + /* returns WS_SUCCESS on success */ /* NOLINTNEXTLINE(misc-no-recursion): bounded by WOLFSSHD_MAX_INCLUDE_DEPTH */ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, @@ -1321,11 +1411,33 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, break; case OPT_HOST_KEY: /* TODO: Add logic to check if file exists? */ - ret = wolfSSHD_ConfigSetHostKeyFile(*conf, value); + ret = CheckNotInMatch(*conf, "HostKey"); + if (ret == WS_SUCCESS) { + ret = wolfSSHD_ConfigSetHostKeyFile(*conf, value); + } + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + else if (*conf != NULL) { + /* Earlier releases accepted (and never used) this placement, + * so ignore-unknown builds keep a migration path. */ + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring HostKey inside a Match block"); + ret = WS_SUCCESS; + } + #endif break; case OPT_HOST_CERT: /* TODO: Add logic to check if file exists? */ - ret = wolfSSHD_ConfigSetHostCertFile(*conf, value); + ret = CheckNotInMatch(*conf, "HostCertificate"); + if (ret == WS_SUCCESS) { + ret = wolfSSHD_ConfigSetHostCertFile(*conf, value); + } + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + else if (*conf != NULL) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring HostCertificate inside a Match block"); + ret = WS_SUCCESS; + } + #endif break; case OPT_PASSWORD_AUTH: ret = HandlePwAuth(*conf, value); @@ -1358,11 +1470,16 @@ static int HandleConfigOption(WOLFSSHD_CONFIG** conf, int opt, (*conf)->heap); break; case OPT_TRUSTED_USER_CA_KEYS: - /* TODO: Add logic to check if file exists? */ + /* Deliberately allowed inside a Match block: the resolved per-user + * value is consumed live at authentication time for OpenSSH + * certificates (CheckPublicKeyUnix/SetupUserTokenWin), so + * Match-scoped CA scoping works and must keep working. */ 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); @@ -1374,51 +1491,85 @@ 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 + #ifdef WOLFSSHD_WIN_STORE_CONFIG 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 */ + #else + case OPT_WIN_USER_STORES: + case OPT_WIN_USER_DW_FLAGS: + case OPT_WIN_USER_PV_PARA: + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring wolfSSH_WinUser* option: requires a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_SUCCESS; + #else + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUser* options require a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_NOT_COMPILED; + #endif + break; + #endif /* WOLFSSHD_WIN_STORE_CONFIG */ case OPT_AUTHORIZED_UPN_DOMAINS: ret = SetListString(&(*conf)->authorizedUPNDomains, full, fullSz, (*conf)->heap); break; - #ifdef WOLFSSH_WINDOWS_CERT_STORE + #ifdef WOLFSSHD_WIN_STORE_CONFIG 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 = SetFileString(&(*conf)->hostKeyStoreSubject, value, - (*conf)->heap); + ret = CheckNotInMatch(*conf, "HostKeyStoreSubject"); + /* use the full line remainder so a CN containing spaces is + * kept instead of being cut at the first token */ + 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: case OPT_HOST_KEY_STORE_SUBJECT: case OPT_HOST_KEY_STORE_FLAGS: + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring HostKeyStore* option: requires a " + "WOLFSSH_WINDOWS_CERT_STORE build"); + ret = WS_SUCCESS; + #else wolfSSH_Log(WS_LOG_ERROR, "[SSHD] HostKeyStore* options require a " "WOLFSSH_WINDOWS_CERT_STORE build"); ret = WS_NOT_COMPILED; + #endif break; - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #endif /* WOLFSSHD_WIN_STORE_CONFIG */ default: break; } @@ -1435,12 +1586,12 @@ static int CountWhitespace(const char* in, int inSz, byte inv) if (in != NULL) { for (; i < inSz; ++i) { if (inv) { - if (WISSPACE(in[i])) { + if (WISSPACE((unsigned char)in[i])) { break; } } else { - if (!WISSPACE(in[i])) { + if (!WISSPACE((unsigned char)in[i])) { break; } } @@ -1466,7 +1617,8 @@ WOLFSSHD_STATIC int ParseConfigLine(WOLFSSHD_CONFIG** conf, const char* l, for (idx = 0; idx < NUM_OPTIONS; ++idx) { sz = (int)WSTRLEN(options[idx].name); - if (lSz >= sz && WSTRNCMP(l, options[idx].name, sz) == 0) { + if (lSz >= sz && WSTRNCMP(l, options[idx].name, sz) == 0 && + (lSz == sz || WISSPACE((unsigned char)l[sz]))) { found = &options[idx]; break; } @@ -1492,13 +1644,33 @@ WOLFSSHD_STATIC int ParseConfigLine(WOLFSSHD_CONFIG** conf, const char* l, } } else { - #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG - wolfSSH_Log(WS_LOG_DEBUG, "[SSHD] ignoring config line %s.", l); - ret = WS_SUCCESS; - #else - wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error parsing config line."); - ret = WS_FATAL_ERROR; - #endif + int isEqForm = 0; + + /* A known keyword in the OpenSSH Keyword=value form must stay a + * fatal error even on builds that ignore unknown lines: dropping a + * directive such as PasswordAuthentication=no would fail open. */ + for (idx = 0; idx < NUM_OPTIONS; ++idx) { + sz = (int)WSTRLEN(options[idx].name); + if (lSz > sz && WSTRNCMP(l, options[idx].name, sz) == 0 && + l[sz] == '=') { + isEqForm = 1; + break; + } + } + if (isEqForm) { + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Keyword=value form is not " + "supported, use \"Keyword value\" : %s.", l); + ret = WS_FATAL_ERROR; + } + else { + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + wolfSSH_Log(WS_LOG_WARN, "[SSHD] ignoring config line %s.", l); + ret = WS_SUCCESS; + #else + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Error parsing config line."); + ret = WS_FATAL_ERROR; + #endif + } } return ret; @@ -1796,8 +1968,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; } } @@ -1837,130 +2011,125 @@ 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; } } return ret; } -#ifdef USE_WINDOWS_API -char* wolfSSHD_ConfigGetWinUserStores(WOLFSSHD_CONFIG* conf) +#ifdef WOLFSSHD_WIN_STORE_CONFIG +/* 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) { int ret = WS_SUCCESS; + char* newValue = NULL; if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } + /* build the replacement before freeing the old value, in case value + * aliases the string currently stored in conf->winUserStores */ + if (ret == WS_SUCCESS) { + ret = CreateString(&newValue, 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); + conf->winUserStores = newValue; } 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) { int ret = WS_SUCCESS; + char* newValue = NULL; if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } + /* build the replacement before freeing the old value, in case value + * aliases the string currently stored in conf->winUserDwFlags */ + if (ret == WS_SUCCESS) { + ret = CreateString(&newValue, 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); + conf->winUserDwFlags = newValue; } 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) { int ret = WS_SUCCESS; + char* newValue = NULL; if (conf == NULL || value == NULL) { ret = WS_BAD_ARGUMENT; } + /* build the replacement before freeing the old value, in case value + * aliases the string currently stored in conf->winUserPvPara */ + if (ret == WS_SUCCESS) { + ret = CreateString(&newValue, 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); + conf->winUserPvPara = newValue; } return ret; } -#endif /* USE_WINDOWS_API */ +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ char* wolfSSHD_ConfigGetUserCAKeysFile(const WOLFSSHD_CONFIG* conf) { @@ -1984,6 +2153,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 */ +const WOLFSSHD_CONFIG* wolfSSHD_ConfigGetNext(const WOLFSSHD_CONFIG* conf) +{ + const 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; @@ -2006,7 +2188,7 @@ static int SetFileString(char** dst, const char* src, void* heap) return ret; } -#ifdef WOLFSSH_WINDOWS_CERT_STORE +#ifdef WOLFSSHD_WIN_STORE_CONFIG char* wolfSSHD_ConfigGetHostKeyStore(const WOLFSSHD_CONFIG* conf) { char* ret = NULL; @@ -2041,7 +2223,7 @@ char* wolfSSHD_ConfigGetHostKeyStoreFlags(const WOLFSSHD_CONFIG* conf) return ret; } -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ int wolfSSHD_ConfigSetHostKeyFile(WOLFSSHD_CONFIG* conf, const char* file) { diff --git a/apps/wolfsshd/configuration.h b/apps/wolfsshd/configuration.h index 554aeba50..83982942c 100644 --- a/apps/wolfsshd/configuration.h +++ b/apps/wolfsshd/configuration.h @@ -32,6 +32,14 @@ typedef struct WOLFSSHD_CONFIG WOLFSSHD_CONFIG; #define WOLFSSHD_STATIC static #endif +/* The Windows cert-store config plumbing (parse, copy, get/set) is plain + * string handling with no Windows dependency, so compile it for unit tests + * on every platform; only the consumers in wolfsshd.c/auth.c need the real + * WOLFSSH_WINDOWS_CERT_STORE build. */ +#if defined(WOLFSSH_WINDOWS_CERT_STORE) || defined(WOLFSSHD_UNIT_TEST) +#define WOLFSSHD_WIN_STORE_CONFIG +#endif + #include "auth.h" /* 0 so that privilege separation is default on after struct memset'd on init */ @@ -62,25 +70,26 @@ 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); +const 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 +#ifdef WOLFSSHD_WIN_STORE_CONFIG 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 */ +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ 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 WOLFSSHD_WIN_STORE_CONFIG +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 /* WOLFSSHD_WIN_STORE_CONFIG */ int wolfSSHD_ConfigSetUserCAKeysFile(WOLFSSHD_CONFIG* conf, const char* file); word16 wolfSSHD_ConfigGetPort(const WOLFSSHD_CONFIG* conf); char* wolfSSHD_ConfigGetAuthKeysFile(const WOLFSSHD_CONFIG* conf); @@ -101,6 +110,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/create_sshd_config.sh b/apps/wolfsshd/test/create_sshd_config.sh index 035e5c920..ebd65a0ac 100755 --- a/apps/wolfsshd/test/create_sshd_config.sh +++ b/apps/wolfsshd/test/create_sshd_config.sh @@ -30,6 +30,18 @@ AuthorizedKeysFile $PWD/authorized_keys_test EOF +# wolfSSHd refuses to start when AuthorizedUPNDomains is set on a build that +# cannot enforce it (wolfSSL without FPKI), so only write the directive when +# the build reports FPKI support. sshd_x509_upn_fail.sh skips itself on such +# builds using the same probe. +. ./wolfssh_options.sh +UPN_DOMAIN_GOOD="" +UPN_DOMAIN_BAD="" +if wolfssh_has FPKI; then + UPN_DOMAIN_GOOD="AuthorizedUPNDomains example" + UPN_DOMAIN_BAD="AuthorizedUPNDomains other.example" +fi + cat < sshd_config_test_x509 Port 22222 Protocol 2 @@ -43,7 +55,7 @@ UseDNS no TrustedUserCAKeys $PWD/../../../keys/ca-cert-ecc.pem HostKey $PWD/../../../keys/server-key.pem HostCertificate $PWD/../../../keys/server-cert.pem -AuthorizedUPNDomains example +$UPN_DOMAIN_GOOD EOF @@ -60,7 +72,7 @@ UseDNS no TrustedUserCAKeys $PWD/../../../keys/ca-cert-ecc.pem HostKey $PWD/../../../keys/server-key.pem HostCertificate $PWD/../../../keys/server-cert.pem -AuthorizedUPNDomains other.example +$UPN_DOMAIN_BAD EOF diff --git a/apps/wolfsshd/test/run_all_sshd_tests.sh b/apps/wolfsshd/test/run_all_sshd_tests.sh index 65cc89aa9..b80d8b1d6 100755 --- a/apps/wolfsshd/test/run_all_sshd_tests.sh +++ b/apps/wolfsshd/test/run_all_sshd_tests.sh @@ -172,6 +172,61 @@ EOF rm -f strictmodes_hostkey.pem sshd_config_test_strictmodes strictmodes_log.txt } +# Negative test: on a build that cannot enforce AuthorizedUPNDomains (wolfSSL +# without FPKI), wolfSSHd must refuse to start rather than silently ignore the +# configured realm policy. The directive is placed inside a Match block so the +# config-node traversal in SetupCTX is exercised, not just the head node. On +# an FPKI build the directive is enforced instead of rejected, so skip. +run_upn_unenforceable_negative_test() { + printf "AuthorizedUPNDomains unenforceable-build negative test ... " + TOTAL=$((TOTAL+1)) + if wolfssh_has FPKI; then + printf "SKIPPED (FPKI build enforces the directive)\n" + SKIPPED=$((SKIPPED+1)) + return + fi + # The host key must clear the secure-file gate (owner-only perms), which + # runs before the UPN gate in SetupCTX; the checked-in key is 0644, so use + # a local mode-600 copy like run_strictmodes_negative_test does. + cp ../../../keys/server-key.pem upn_hostkey.pem + chmod 600 upn_hostkey.pem + cat < sshd_config_test_upn_nofpki +Port 22623 +UsePrivilegeSeparation no +HostKey upn_hostkey.pem +Match User $USER +AuthorizedUPNDomains example +EOF + rm -f upn_nofpki_log.txt + # Without a timeout wrapper a regression that lets the daemon start would + # hang the suite indefinitely, so skip rather than run unbounded. + if ! command -v timeout >/dev/null 2>&1; then + printf "SKIPPED (no timeout command)\n" + SKIPPED=$((SKIPPED+1)) + rm -f upn_hostkey.pem sshd_config_test_upn_nofpki + return + fi + timeout 30 ../wolfsshd -D -d -f sshd_config_test_upn_nofpki \ + -E upn_nofpki_log.txt + # Match the fail-closed wording only: the WOLFSSH_IGNORE_UNKNOWN_CONFIG + # branch logs "Ignoring AuthorizedUPNDomains ... cannot enforce it" and + # keeps running, which must not pass as the startup refusal. Also require + # that the host key loaded: a "Refusing to load" failure would exit before + # the UPN gate. + if grep -q "AuthorizedUPNDomains is set" upn_nofpki_log.txt && + grep -q "but this build cannot enforce it" upn_nofpki_log.txt && + ! grep -q "Refusing to load" upn_nofpki_log.txt; then + printf "PASSED\n" + else + printf "FAILED!\n" + cat upn_nofpki_log.txt + rm -f upn_hostkey.pem sshd_config_test_upn_nofpki upn_nofpki_log.txt + stop_wolfsshd + exit 1 + fi + rm -f upn_hostkey.pem sshd_config_test_upn_nofpki upn_nofpki_log.txt +} + # Negative authorized_keys StrictModes test: a group/world writable # authorized_keys file must make public-key authentication fail (exercises the # StrictModes branch in SearchForPubKey). Uses the already-running local sshd, @@ -412,11 +467,12 @@ else run_test "sshd_permitroot_prohibit_password.sh" run_test "sshd_permitroot_forced_cmd.sh" run_strictmodes_negative_test + run_upn_unenforceable_negative_test run_test "sshd_login_grace_test.sh" run_test "sshd_privdrop_fail_test.sh" else printf "Skipping tests that need to setup local SSHD\n" - SKIPPED=$((SKIPPED+9)) + SKIPPED=$((SKIPPED+10)) fi # these tests run with X509 sshd-config loaded diff --git a/apps/wolfsshd/test/test_configuration.c b/apps/wolfsshd/test/test_configuration.c index 326843663..a6e88efa1 100644 --- a/apps/wolfsshd/test/test_configuration.c +++ b/apps/wolfsshd/test/test_configuration.c @@ -290,6 +290,34 @@ static int test_ParseConfigLine(void) /* Whitespace tests. */ {"Extra leading whitespace", "Port 22", 0}, {"Extra trailing whitespace", "Port 22 \n", 0}, + {"Tab delimiter", "Port\t22", 0}, + {"Trailing tabs", "Port 22\t\t\n", 0}, + + /* The option matcher requires whitespace (or end of line) after the + * matched name, so an unknown name that extends a real one must not + * prefix-match it. Ignore-unknown builds accept such lines with a + * warning, so only assert rejection where it is observable. */ + #ifndef WOLFSSH_IGNORE_UNKNOWN_CONFIG + {"Unknown extension of Port", "PortFoo 22", 1}, + {"Unknown extension of HostKey", "HostKeyFoo /tmp/x", 1}, + {"Unknown extension of HostKeyStore", "HostKeyStoreX MY", 1}, + {"Unknown extension of TrustedUserCAStore", + "wolfSSH_TrustedUserCAStoreX yes", 1}, + #endif + /* A known keyword in Keyword=value form is a hard error on every + * build; ignoring it would silently drop the directive. */ + {"Keyword=value form is rejected", "Port=22", 1}, + + /* The two store-trust toggles follow the same yes/no/invalid + * convention as every other boolean option. Note: on builds without + * WOLFSSH_CERTS support these still parse; only SetupCTX rejects + * them, so parse success is the right expectation everywhere. */ + {"System CA yes", "wolfSSH_TrustedSystemCAKeys yes", 0}, + {"System CA no", "wolfSSH_TrustedSystemCAKeys no", 0}, + {"System CA invalid", "wolfSSH_TrustedSystemCAKeys wolfsshd", 1}, + {"User CA store yes", "wolfSSH_TrustedUserCAStore yes", 0}, + {"User CA store no", "wolfSSH_TrustedUserCAStore no", 0}, + {"User CA store invalid", "wolfSSH_TrustedUserCAStore wolfsshd", 1}, /* Privilege separation tests. */ {"Privilege separation yes", "UsePrivilegeSeparation yes", 0}, @@ -421,6 +449,19 @@ static int test_ConfigCopy(void) /* set to non-default (default is on) so a dropped copy is detected */ if (ret == WS_SUCCESS) ret = PCL("StrictModes no"); + /* CA trust flags, non-default so a dropped copy is detected */ + if (ret == WS_SUCCESS) ret = PCL("wolfSSH_TrustedSystemCAKeys yes"); + if (ret == WS_SUCCESS) ret = PCL("wolfSSH_TrustedUserCAStore yes"); + +#ifdef WOLFSSHD_WIN_STORE_CONFIG + if (ret == WS_SUCCESS) ret = PCL("HostKeyStore MY"); + if (ret == WS_SUCCESS) ret = PCL("HostKeyStoreSubject wolfSSH Host"); + if (ret == WS_SUCCESS) ret = PCL("HostKeyStoreFlags 0x1000"); + if (ret == WS_SUCCESS) ret = PCL("wolfSSH_WinUserStores MY,Root"); + if (ret == WS_SUCCESS) ret = PCL("wolfSSH_WinUserDwFlags 0x1"); + if (ret == WS_SUCCESS) ret = PCL("wolfSSH_WinUserPvPara subjectName"); +#endif + /* trigger ConfigCopy via Match; conf advances to the new node */ if (ret == WS_SUCCESS) ret = PCL("Match User testuser"); #undef PCL @@ -524,6 +565,52 @@ static int test_ConfigCopy(void) ret = WS_FATAL_ERROR; } + /* CA trust flags must survive the copy */ + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetSystemCA(match) != 1) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetUserCAStore(match) != 1) + ret = WS_FATAL_ERROR; + } + +#ifdef WOLFSSHD_WIN_STORE_CONFIG + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetHostKeyStore(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetHostKeyStore(match), "MY") != 0) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetHostKeyStoreSubject(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetHostKeyStoreSubject(match), + "wolfSSH Host") != 0) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetHostKeyStoreFlags(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetHostKeyStoreFlags(match), + "0x1000") != 0) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetWinUserStores(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetWinUserStores(match), "MY,Root") != 0) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetWinUserDwFlags(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetWinUserDwFlags(match), "0x1") != 0) + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS) { + if (wolfSSHD_ConfigGetWinUserPvPara(match) == NULL || + XSTRCMP(wolfSSHD_ConfigGetWinUserPvPara(match), + "subjectName") != 0) + ret = WS_FATAL_ERROR; + } +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ + wolfSSHD_ConfigFree(head); return ret; } @@ -4417,8 +4504,245 @@ 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; +} + +/* Every CheckNotInMatch-guarded option must fail to parse inside a Match + * block with WS_BAD_ARGUMENT (the value CheckNotInMatch returns, so a + * rejection cannot be confused with an unrecognised option name), while the + * same line parses successfully at global scope as a positive control. + * TrustedUserCAKeys is deliberately absent: its per-user resolved value is + * honored live at authentication time, so a Match-scoped setting is + * supported and must keep parsing (asserted at the end). */ +static int test_ConfigGlobalOnlyOptionsInMatch(void) +{ + int ret = WS_SUCCESS; + int i; + int rc; + WOLFSSHD_CONFIG* head; + WOLFSSHD_CONFIG* conf; + typedef struct { + const char* line; + int expectedInMatch; + } GLOBAL_ONLY_VECTOR; + /* HostKey/HostCertificate are rejected in a Match block on every build + * except WOLFSSH_IGNORE_UNKNOWN_CONFIG, where HandleConfigOption + * downgrades them to a warning and returns WS_SUCCESS as a documented + * migration path. */ + static const GLOBAL_ONLY_VECTOR lines[] = { + { "wolfSSH_TrustedSystemCAKeys yes", WS_BAD_ARGUMENT }, + { "wolfSSH_TrustedUserCAStore yes", WS_BAD_ARGUMENT }, +#ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + { "HostKey /etc/ssh/host_key", WS_SUCCESS }, + { "HostCertificate /etc/ssh/host_cert.pem", WS_SUCCESS }, +#else + { "HostKey /etc/ssh/host_key", WS_BAD_ARGUMENT }, + { "HostCertificate /etc/ssh/host_cert.pem", WS_BAD_ARGUMENT }, +#endif +#ifdef WOLFSSHD_WIN_STORE_CONFIG + { "HostKeyStore MY", WS_BAD_ARGUMENT }, + { "HostKeyStoreSubject wolfSSH Host", WS_BAD_ARGUMENT }, + { "HostKeyStoreFlags 0x1000", WS_BAD_ARGUMENT }, + { "wolfSSH_WinUserStores CERT_STORE_PROV_SYSTEM", WS_BAD_ARGUMENT }, + { "wolfSSH_WinUserDwFlags LOCAL_MACHINE", WS_BAD_ARGUMENT }, + { "wolfSSH_WinUserPvPara SSH_UserCA", WS_BAD_ARGUMENT }, +#endif + }; + +#define PCL(s) ParseConfigLine(&conf, s, (int)WSTRLEN(s), 0) + for (i = 0; i < (int)(sizeof(lines) / sizeof(*lines)); i++) { + Log(" Testing scenario: '%s' in Match block.", lines[i].line); + head = wolfSSHD_ConfigNew(NULL); + conf = head; + if (head == NULL) { + ret = WS_MEMORY_E; + } + /* positive control: the same line is valid at global scope */ + if (ret == WS_SUCCESS) { + rc = ParseConfigLine(&conf, lines[i].line, + (int)WSTRLEN(lines[i].line), 0); + if (rc != WS_SUCCESS) { + Log(" global-scope control parse failed (%d).", rc); + ret = WS_FATAL_ERROR; + } + } + if (ret == WS_SUCCESS) { + ret = PCL("Match User testuser"); + } + if (ret == WS_SUCCESS) { + rc = ParseConfigLine(&conf, lines[i].line, + (int)WSTRLEN(lines[i].line), 0); + if (rc != lines[i].expectedInMatch) { + Log(" expected %d, got %d.", lines[i].expectedInMatch, rc); + ret = WS_FATAL_ERROR; + } + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(head); + if (ret != WS_SUCCESS) { + break; + } + } + + /* TrustedUserCAKeys stays legal inside a Match block */ + if (ret == WS_SUCCESS) { + Log(" Testing scenario: TrustedUserCAKeys in Match block " + "accepted."); + head = wolfSSHD_ConfigNew(NULL); + conf = head; + if (head == NULL) { + ret = WS_MEMORY_E; + } + if (ret == WS_SUCCESS) { + ret = PCL("Match User testuser"); + } + if (ret == WS_SUCCESS) { + ret = PCL("TrustedUserCAKeys /etc/ssh/ca.pub"); + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + wolfSSHD_ConfigFree(head); + } +#undef PCL + + return ret; +} + +#ifdef WOLFSSHD_WIN_STORE_CONFIG +/* NULL-argument and replace-existing coverage for the three wolfSSH_WinUser* + * setters, modelled on test_ConfigSetAuthKeysFile. The replace path frees + * the previous value, which the sanitizer builds verify. */ +static int test_ConfigSetWinUserOptions(void) +{ + int ret = WS_SUCCESS; + WOLFSSHD_CONFIG* conf; + + conf = wolfSSHD_ConfigNew(NULL); + if (conf == NULL) { + ret = WS_MEMORY_E; + } + + if (ret == WS_SUCCESS) { + Log(" Testing scenario: WinUser setters NULL arguments."); + if (wolfSSHD_ConfigSetWinUserStores(NULL, "x") != WS_BAD_ARGUMENT || + wolfSSHD_ConfigSetWinUserStores(conf, NULL) != WS_BAD_ARGUMENT || + wolfSSHD_ConfigSetWinUserDwFlags(NULL, "x") != WS_BAD_ARGUMENT || + wolfSSHD_ConfigSetWinUserDwFlags(conf, NULL) != WS_BAD_ARGUMENT || + wolfSSHD_ConfigSetWinUserPvPara(NULL, "x") != WS_BAD_ARGUMENT || + wolfSSHD_ConfigSetWinUserPvPara(conf, NULL) != WS_BAD_ARGUMENT) { + ret = WS_FATAL_ERROR; + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + } + + if (ret == WS_SUCCESS) { + Log(" Testing scenario: WinUser setters replace existing value."); + if (wolfSSHD_ConfigSetWinUserStores(conf, + "CERT_STORE_PROV_SYSTEM") != WS_SUCCESS || + wolfSSHD_ConfigSetWinUserStores(conf, "second") != WS_SUCCESS || + WSTRCMP(wolfSSHD_ConfigGetWinUserStores(conf), "second") != 0) { + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS && + (wolfSSHD_ConfigSetWinUserDwFlags(conf, + "LOCAL_MACHINE") != WS_SUCCESS || + wolfSSHD_ConfigSetWinUserDwFlags(conf, + "CURRENT_USER") != WS_SUCCESS || + WSTRCMP(wolfSSHD_ConfigGetWinUserDwFlags(conf), + "CURRENT_USER") != 0)) { + ret = WS_FATAL_ERROR; + } + if (ret == WS_SUCCESS && + (wolfSSHD_ConfigSetWinUserPvPara(conf, + "SSH_UserCA") != WS_SUCCESS || + wolfSSHD_ConfigSetWinUserPvPara(conf, + "OtherStore") != WS_SUCCESS || + WSTRCMP(wolfSSHD_ConfigGetWinUserPvPara(conf), + "OtherStore") != 0)) { + ret = WS_FATAL_ERROR; + } + Log(ret == WS_SUCCESS ? " PASSED.\n" : " FAILED.\n"); + } + + wolfSSHD_ConfigFree(conf); + + return ret; +} +#endif /* WOLFSSHD_WIN_STORE_CONFIG */ + +/* Exercises the exported per-user AuthorizedKeysFile predicate that decides + * whether an entry in the resolved file is an implicit user binding. */ +static int test_AuthKeysPatternIsPerUser(void) +{ + int ret = WS_SUCCESS; + int i; + int got; + static const struct { + const char* desc; + const char* pattern; + int expect; + } vectors[] = { + {"NULL uses the built-in per-user default", NULL, 1}, + {"empty string uses the built-in default", "", 1}, + {"relative path resolves under home", ".ssh/authorized_keys", 1}, + {"absolute shared file", "/etc/ssh/authorized_keys_all", 0}, + {"absolute with %u component", "/etc/ssh/keys/%u", 1}, + {"absolute with %h component", "/etc/ssh/%h/keys", 1}, + {"absolute with embedded %u", "/etc/ssh/keys%u", 1}, + {"absolute with only literal %%u", "/etc/ssh/%%u", 0}, + /* relative, so per-user regardless of its percent content */ + {"relative with literal percents", "%%%u", 1}, + {"absolute literal percent then %u", "/a%%%u", 1}, + /* the %% skip must not let the 'u' after a literal percent pair + * count as a %u token */ + {"absolute double literal percent then u", "/a%%%%u", 0}, + {"absolute %u escaped by ..", "/etc/ssh/%u/../shared", 0}, + {"relative escaped by ..", "../shared", 0}, +#ifdef _WIN32 + /* Windows-rooted forms are only absolute on a _WIN32 build; these + * are the security-relevant shared-file shapes that must NOT + * classify as per-user there */ + {"drive-rooted shared file", + "C:\\ProgramData\\ssh\\authorized_keys_all", 0}, + {"UNC shared file", "\\\\server\\share\\authorized_keys", 0}, + {"drive-rooted with %u", "C:\\ProgramData\\ssh\\%u", 1}, + {"drive-rooted %u escaped by ..", "C:\\keys\\%u\\..\\shared", 0}, + /* drive-relative resolves under home and fails closed there */ + {"drive-relative path", "C:foo", 1}, +#endif + }; + + for (i = 0; i < (int)(sizeof(vectors) / sizeof(*vectors)); i++) { + Log(" Testing scenario: %s.", vectors[i].desc); + got = wolfSSHD_AuthKeysPatternIsPerUser(vectors[i].pattern); + if (got != vectors[i].expect) { + Log(" got %d expected %d. FAILED.\n", got, vectors[i].expect); + ret = WS_FATAL_ERROR; + break; + } + Log(" PASSED.\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; @@ -4490,6 +4814,15 @@ static int test_MatchUPNToUser(void) {"allowlist, empty domain", "alice", "alice@", "corp.example", 0, 0}, {"allowlist, wrong local part", "bob", "alice@corp.example", "corp.example", 0, 0}, + /* Windows account names are case-insensitive and the local part + * match follows suit there; on Unix the match stays exact. */ +#ifdef _WIN32 + {"case-differing local part", "ALICE", "alice@corp.example", + "corp.example", 1, 0}, +#else + {"case-differing local part", "ALICE", "alice@corp.example", + "corp.example", 0, 0}, +#endif {"allowlist multi, first", "alice", "alice@corp.example", "corp.example other.example", 1, 0}, {"allowlist multi, second", "alice", "alice@other.example", @@ -6147,6 +6480,12 @@ const TEST_CASE testCases[] = { TEST_DECL(test_ConfigDefaults), TEST_DECL(test_PermitRootProhibitPassword), TEST_DECL(test_ParseConfigLine), + TEST_DECL(test_ConfigOptionPrefixOrder), + TEST_DECL(test_ConfigGlobalOnlyOptionsInMatch), +#ifdef WOLFSSHD_WIN_STORE_CONFIG + TEST_DECL(test_ConfigSetWinUserOptions), +#endif + TEST_DECL(test_AuthKeysPatternIsPerUser), 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 54ce66724..3d29194f4 100644 --- a/apps/wolfsshd/wolfsshd.c +++ b/apps/wolfsshd/wolfsshd.c @@ -22,6 +22,19 @@ #include #endif +#ifdef _WIN32 + /* ConPTY (HPCON, CreatePseudoConsole) requires a Windows 10 1809 (RS5) + * API target; mingw-w64 gates the declarations on NTDDI_VERSION */ + #if !defined(_WIN32_WINNT) || _WIN32_WINNT < 0x0A00 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0A00 + #endif + #if !defined(NTDDI_VERSION) || NTDDI_VERSION < 0x0A000006 + #undef NTDDI_VERSION + #define NTDDI_VERSION 0x0A000006 /* NTDDI_WIN10_RS5 */ + #endif +#endif + #ifdef WOLFSSL_USER_SETTINGS #include #else @@ -37,17 +50,44 @@ #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 + #ifndef CERT_SYSTEM_STORE_CURRENT_SERVICE + #define CERT_SYSTEM_STORE_CURRENT_SERVICE 0x00040000 + #endif + #ifndef CERT_SYSTEM_STORE_SERVICES + #define CERT_SYSTEM_STORE_SERVICES 0x00050000 + #endif + #ifndef CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY + #define CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY 0x00070000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY 0x00080000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE 0x00090000 + #endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #define WOLFSSH_TEST_SERVER @@ -231,15 +271,75 @@ static void interruptCatch(int in) #define WGETPID getpid #endif +/* Syslog-style suppression of consecutively repeated lines, so no single + * repeating message can grow the log without bound (some library notices + * fire once per received packet, which a peer controls). A repeat is counted + * instead of written and the count is emitted when a different message + * arrives, when a connection ends, or at shutdown. The state is per process: + * each forked connection child (POSIX) suppresses its own stream; on Windows + * the connection threads share it under logRepeatLock. */ +static char lastLogMsg[256]; +static unsigned long logRepeats = 0; +#ifdef _WIN32 +static SRWLOCK logRepeatLock = SRWLOCK_INIT; +#endif + +/* Emit any pending "last message repeated N times" count. Called when a + * connection ends and at daemon shutdown so a trailing repeat streak is not + * lost when the process exits without a further distinct message. */ +static void wolfSSHDLoggingFlush(void) +{ +#ifdef _WIN32 + AcquireSRWLockExclusive(&logRepeatLock); +#endif + if (logRepeats > 0 && logFile != NULL) { + fprintf(logFile, "[PID %lu]: last message repeated %lu times\n", + (unsigned long)WGETPID(), logRepeats); + fflush(logFile); + } + logRepeats = 0; + lastLogMsg[0] = '\0'; +#ifdef _WIN32 + ReleaseSRWLockExclusive(&logRepeatLock); +#endif +} + /* 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) { - 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 */ - fflush(logFile); + /* 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 (logFile == NULL) { + return; + } + if (lvl == WS_LOG_ERROR || lvl == WS_LOG_WARN || debugMode) { +#ifdef _WIN32 + AcquireSRWLockExclusive(&logRepeatLock); +#endif + /* The comparison is capped at the buffer size, so lines identical + * through the cap count as repeats. */ + if (lastLogMsg[0] != '\0' && + WSTRNCMP(str, lastLogMsg, sizeof(lastLogMsg) - 1) == 0) { + logRepeats++; + } + else { + if (logRepeats > 0) { + fprintf(logFile, + "[PID %lu]: last message repeated %lu times\n", + (unsigned long)WGETPID(), logRepeats); + logRepeats = 0; + } + WSNPRINTF(lastLogMsg, sizeof(lastLogMsg), "%s", str); + fprintf(logFile, "[PID %lu]: %s\n", (unsigned long)WGETPID(), str); + /* flush so each line is visible immediately, e.g. to a consumer + * reading the log file while the daemon is still running */ + fflush(logFile); + } +#ifdef _WIN32 + ReleaseSRWLockExclusive(&logRepeatLock); +#endif } } @@ -353,60 +453,235 @@ 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 +/* Returns 1 only for the store hives that need elevation to write: the three + * LOCAL_MACHINE locations. Every other hive (per-user, per-service, + * HKEY_USERS and per-user group policy) can be written without elevation by + * the account it belongs to, so trust anchors or host keys placed there can + * be replaced by anything running as that account. Shared by the user CA + * store and host key store checks so the two cannot diverge. */ +static int IsElevationProtectedHive(word32 dwFlags) +{ + return dwFlags == (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE || + dwFlags == (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY || + dwFlags == (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE; +} + +/* 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, 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), heap, DYNTYPE_CERT); + if (dCert == NULL) { + return CERT_CA_UNKNOWN; + } +#else + dCert = &sdCert; +#endif + + 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, 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 every backslash-separated component is + * compared case-insensitively rather than only the final one. Trailing + * whitespace is ignored and an empty component fails closed: the registry + * tolerates a redundant trailing backslash, so "Root\" resolves to the same + * key as "Root" and must be refused the same way. */ +static int IsWinPublicTrustStoreName(const char* name) +{ + /* Includes every store Windows itself populates (Trusted Root Program, + * Windows Update, Group Policy). Revisit for new OS-managed stores when + * supporting a new Windows release. */ + static const char* const deny[] = { + "Root", "AuthRoot", "CA", "Disallowed", "TrustedPublisher", "trust", + "SmartCardRoot", "ClientAuthIssuer", "TrustedPeople", "TrustedDevices", + "FlightRoot", "TestSignRoot" + }; + const char* comp; + const char* sep; + word32 nameLen; + word32 len; + word32 i; + + if (name == NULL) { + /* fail closed */ + return 1; + } + + nameLen = (word32)WSTRLEN(name); + while (nameLen > 0 && (name[nameLen - 1] == ' ' || + name[nameLen - 1] == '\t')) { + nameLen--; + } + if (nameLen == 0) { + /* fail closed on an empty or all-whitespace name */ + return 1; + } + + comp = name; + for (;;) { + sep = WSTRCHR(comp, '\\'); + if (sep != NULL && (word32)(sep - name) >= nameLen) { + /* the separator sits in the trimmed-off tail; treat it as + * absent so len below is always bounded by nameLen and cannot + * underflow */ + sep = NULL; + } + if (sep != NULL) { + len = (word32)(sep - comp); + } + else { + len = nameLen - (word32)(comp - name); + } + if (len == 0) { + /* an empty component ("Root\", "\Root", "a\\b") changes nothing + * about the key the registry resolves; fail closed */ + return 1; + } + for (i = 0; i < (word32)(sizeof(deny) / sizeof(*deny)); i++) { + if (len == (word32)WSTRLEN(deny[i]) && + WSTRNCASECMP(comp, deny[i], len) == 0) { + return 1; + } + } + if (sep == NULL) { + break; + } + comp = sep + 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. */ -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; 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; + word32 notX509 = 0; + int isCA; 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. 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. */ + /* wolfSSH_Log truncates at WOLFSSH_DEFAULT_LOG_WIDTH (120), so every + * multi-sentence diagnostic here is split into several calls with the + * actionable text in its own line. */ 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."); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Set wolfSSH_WinUserPvPara to a store holding nothing but " + "the client CA certs to trust, e.g. 'SSH_UserCA'."); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Do not use the public 'Root', 'AuthRoot' or 'CA' stores."); return WS_BAD_ARGUMENT; } - /* Only the system-store provider is supported here. */ + if (IsWinPublicTrustStoreName(storeNameStr)) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserPvPara='%.64s' names a Windows system " + "trust store.", storeNameStr); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Every CA it holds would become an SSH login authority. " + "Use a store created for this purpose instead."); + return WS_BAD_ARGUMENT; + } + + /* 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_INFO, - "[SSHD] wolfSSH_WinUserStores='%s' ignored; only " + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_WinUserStores='%.48s' is not supported; only " "CERT_STORE_PROV_SYSTEM is supported", providerStr); + 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; 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; - } - } + /* 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."); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Set wolfSSH_WinUserDwFlags, normally to LOCAL_MACHINE."); + return WS_BAD_ARGUMENT; + } + if (wolfSSH_CertStoreLocationFromName(dwFlagsStr, &dwFlags) + != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized user CA store flags '%s'", dwFlagsStr); + return WS_BAD_ARGUMENT; } + if (!IsElevationProtectedHive(dwFlags)) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] wolfSSH_WinUserDwFlags selects a store hive that its own " + "account can write to without elevation."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] 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 certs " + "need not carry a client authentication EKU."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] 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) { @@ -419,15 +694,20 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, 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, wStoreName); if (hStore == NULL) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] Unable to open user CA cert store '%s', error %lu", + "[SSHD] Unable to open user CA cert store '%.48s', error %lu", storeNameStr, (unsigned long)GetLastError()); WFREE(wStoreName, heap, DYNTYPE_SSHD); return WS_FATAL_ERROR; @@ -441,6 +721,30 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, } 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, heap); + if (isCA != CERT_CA_YES) { + skipped++; + 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, @@ -448,8 +752,9 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, (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; } @@ -459,22 +764,68 @@ static int LoadUserCACertsFromStore(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX* ctx, CertCloseStore(hStore, 0); WFREE(wStoreName, heap, DYNTYPE_SSHD); + /* Counts and location go on their own lines: wolfSSH_Log formats into + * a 120 byte buffer, and one line carrying the %.48s store name plus + * the counts would be cut short right where the numbers are. */ if (loaded == 0) { wolfSSH_Log(WS_LOG_ERROR, - "[SSHD] No usable CA certificates found in store '%s'", + "[SSHD] No usable CA certificates found in store '%.48s'", storeNameStr); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Store entries not loaded: %u not a CA, %u rejected, " + "%u not X.509", skipped, rejected, notX509); ret = WS_FATAL_ERROR; } else { wolfSSH_Log(WS_LOG_INFO, - "[SSHD] Loaded %u CA certificate(s) from store '%s'", + "[SSHD] Trusting %u CA certificate(s) from store '%.48s'", loaded, storeNameStr); + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Store location 0x%08lx used for client authentication", + (unsigned long)dwFlags); + if (skipped != 0 || rejected != 0 || notX509 != 0) { + wolfSSH_Log(WS_LOG_INFO, + "[SSHD] Store entries not loaded: %u not a CA, %u rejected " + "as a root CA, %u not X.509", skipped, rejected, notX509); + } + if (rejected > 0) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] %u CA certificate(s) in store '%.48s' could not be " + "loaded; the trust anchor set is incomplete", rejected, + storeNameStr); + } } return ret; } #endif /* WOLFSSH_CERTS && WOLFSSH_WINDOWS_CERT_STORE */ +#if defined(WOLFSSH_CERTS) && (defined(_WIN32) || defined(WOLFSSL_FPKI)) +/* Returns non-zero when any config node configures a certificate trust + * anchor. TrustedUserCAKeys may live inside a Match block, so the whole list + * must be walked; the two store flags are global-only and live on the head + * node. */ +static int AnyNodeHasCertTrustAnchor(const WOLFSSHD_CONFIG* conf) +{ + const WOLFSSHD_CONFIG* cur; + int found = 0; + + if (wolfSSHD_ConfigGetUserCAStore(conf) || + wolfSSHD_ConfigGetSystemCA(conf)) { + found = 1; + } + cur = conf; + while (!found && cur != NULL) { + if (wolfSSHD_ConfigGetUserCAKeysFile(cur) != NULL) { + found = 1; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + + return found; +} +#endif /* WOLFSSH_CERTS && (_WIN32 || WOLFSSL_FPKI) */ + /* Initializes and sets up the WOLFSSH_CTX struct based on the configure options * return WS_SUCCESS on success */ @@ -517,67 +868,101 @@ 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); +#if defined(WOLFSSH_CERTS) && defined(WOLFSSH_WINDOWS_CERT_STORE) + char* hostKeyStore; + char* hostKeyStoreSubject; + char* hostKeyStoreFlags; - 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)"); + hostKeyStore = wolfSSHD_ConfigGetHostKeyStore(conf); + hostKeyStoreSubject = wolfSSHD_ConfigGetHostKeyStoreSubject(conf); + hostKeyStoreFlags = wolfSSHD_ConfigGetHostKeyStoreFlags(conf); if (hostKeyStore != NULL && hostKeyStoreSubject == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] HostKeyStore set but HostKeyStoreSubject is missing"); ret = WS_BAD_ARGUMENT; } + /* The location is mandatory for the same reason wolfSSH_WinUserDwFlags + * is: the per-user hive is writable by the daemon's own account + * without elevation, so silently defaulting to CURRENT_USER would let + * anything running as that account install a host key. */ + else if (hostKeyStore != NULL && hostKeyStoreFlags == NULL) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] HostKeyStore set but HostKeyStoreFlags is missing."); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Set the store location explicitly, normally " + "LOCAL_MACHINE."); + 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; + } + /* 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."); + wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Use one or the other."); + ret = WS_BAD_ARGUMENT; + } 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; - } - } + word32 dwFlags = 0; + int storeNameLen = 0; + int subjectNameLen = 0; + + if (wolfSSH_CertStoreLocationFromName(hostKeyStoreFlags, &dwFlags) + != WS_SUCCESS) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Unrecognized host key store flags '%s'", + hostKeyStoreFlags); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS && !IsElevationProtectedHive(dwFlags)) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] HostKeyStoreFlags selects a store hive that its " + "own account can write to without elevation."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] LOCAL_MACHINE is the safer location for the host " + "key."); } /* 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( @@ -588,12 +973,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) { @@ -611,20 +999,11 @@ 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)"); + char* hostKey; + hostKey = wolfSSHD_ConfigGetHostKeyFile(conf); if (hostKey == NULL) { wolfSSH_Log(WS_LOG_ERROR, "[SSHD] No host private key set"); ret = WS_BAD_ARGUMENT; @@ -787,21 +1166,44 @@ 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_WARN, + "[SSHD] WARNING: wolfSSH_TrustedSystemCAKeys makes every CA in " + "the OS trust store an SSH user auth authority."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Any cert issued by any of them whose subject matches a " + "local account name can log in as that account."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Use this only when the OS trust store holds solely your " + "organization's CA."); + #ifdef WOLFSSH_NO_FPKI + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: built without FPKI profile checking, so peer " + "certs need not carry a client authentication EKU."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] 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"); + 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; } } @@ -809,7 +1211,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; } @@ -833,7 +1235,23 @@ 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) + /* 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] A wolfSSH_WinUser* option is set but " + "wolfSSH_TrustedUserCAStore is not enabled,"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] so it would have no effect."); + ret = WS_BAD_ARGUMENT; + } + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetUserCAStore(conf)) { ret = LoadUserCACertsFromStore(conf, *ctx, heap); } @@ -846,6 +1264,133 @@ 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 && AnyNodeHasCertTrustAnchor(conf)) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] WARNING: client certificates are bound to an account by " + "subject CN only."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Any CA in the trusted user CA set may assert any CN, so " + "keep that set narrow."); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Build wolfSSL with FPKI for UPN binding and " + "AuthorizedUPNDomains."); + } + + /* With CN-only binding, the OS trust store holds every commercial root + * CA, any of which could mint a certificate whose CN names a local + * account. Require every account to also have a per-user + * AuthorizedKeysFile so a subject CN match alone is never sufficient to + * log in; the exact certificate match in that file is the real binding. + * The curated wolfSSH_TrustedUserCAStore path is not held to this because + * its store is admin-created and the public store names are refused. */ + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + const WOLFSSHD_CONFIG* cur; + word32 node = 0; + + cur = conf; + while (cur != NULL) { + if (!wolfSSHD_ConfigGetAuthKeysFileSet(cur) || + !wolfSSHD_AuthKeysPatternIsPerUser( + wolfSSHD_ConfigGetAuthKeysFile(cur))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys with CN-only cert " + "binding requires a per-user AuthorizedKeysFile"); + if (node == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] for every account; the global config has " + "none set before the first Match block."); + } + else { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] for every account; Match block %u has none " + "(directives after a Match block do not apply to " + "it).", node); + } + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Set AuthorizedKeysFile, use " + "wolfSSH_TrustedUserCAStore with a curated store,"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] or build wolfSSL with FPKI."); + ret = WS_BAD_ARGUMENT; + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + node++; + } + } + #endif + + #if defined(WOLFSSH_CERTS) && defined(WOLFSSL_FPKI) + /* The FPKI mirror of the gate above: with UPN binding, the realm + * allowlist is what constrains which CA-issued identities may log in. + * When the whole OS trust store is a login authority, require every + * config node to either set AuthorizedUPNDomains or carry its own + * per-user AuthorizedKeysFile (a shared file binds the certificate to + * nothing, so it does not qualify, matching the CN gate above). Note + * AuthorizedUPNDomains constrains the certificate's own UPN realm, not + * which trusted CA issued it: any CA in the OS store willing to emit a + * UPN in an allowed realm still satisfies it. The per-user + * AuthorizedKeysFile arm is the only one that adds an exact-certificate + * second factor. */ + if (ret == WS_SUCCESS && wolfSSHD_ConfigGetSystemCA(conf)) { + const WOLFSSHD_CONFIG* cur; + const char* domains; + + cur = conf; + while (cur != NULL) { + domains = wolfSSHD_ConfigGetAuthorizedUPNDomains(cur); + if ((domains == NULL || *domains == '\0') && + (!wolfSSHD_ConfigGetAuthKeysFileSet(cur) || + !wolfSSHD_AuthKeysPatternIsPerUser( + wolfSSHD_ConfigGetAuthKeysFile(cur)))) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] wolfSSH_TrustedSystemCAKeys requires " + "AuthorizedUPNDomains or a per-user"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] AuthorizedKeysFile on every config node."); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] Note AuthorizedUPNDomains constrains the UPN " + "realm only, not the issuing CA;"); + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] use it only when the OS trust store holds " + "solely your organization's CA."); + ret = WS_BAD_ARGUMENT; + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + } + + /* States a fixed configuration property, so notice it once here at + * startup rather than from the peer-triggered auth path (the log + * callback writes WARN unconditionally, so a per-attempt WARN would let + * a peer grow the log). Only emitted when a certificate trust anchor is + * actually configured; with no CA there is no UPN check to relax. */ + if (ret == WS_SUCCESS && AnyNodeHasCertTrustAnchor(conf)) { + const WOLFSSHD_CONFIG* cur; + const char* domains; + + cur = conf; + while (cur != NULL) { + domains = wolfSSHD_ConfigGetAuthorizedUPNDomains(cur); + if (domains == NULL || *domains == '\0') { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] AuthorizedUPNDomains not set on at least one " + "config node;"); + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] the certificate UPN domain is not checked " + "there."); + break; + } + cur = wolfSSHD_ConfigGetNext(cur); + } + } + #endif /* WOLFSSH_CERTS && WOLFSSL_FPKI */ + /* load in CA certs from file set */ if (ret == WS_SUCCESS) { char* caCert = wolfSSHD_ConfigGetUserCAKeysFile(conf); @@ -908,6 +1453,59 @@ static int SetupCTX(WOLFSSHD_CONFIG* conf, WOLFSSH_CTX** ctx, } #endif + /* AuthorizedUPNDomains is only enforced by the FPKI UPN check, which + * also needs certificate support. Fail startup rather than silently + * ignore a configured realm policy, unless the build opts into + * WOLFSSH_IGNORE_UNKNOWN_CONFIG, which downgrades not-compiled-in + * directives to a warning the same way the HostKeyStore* and + * wolfSSH_WinUser* handlers do. Check every config node since the + * directive may sit in a Match block. */ +#if !defined(WOLFSSL_FPKI) || !defined(WOLFSSH_CERTS) + if (ret == WS_SUCCESS) { + const WOLFSSHD_CONFIG* upnCur; + word32 upnNode = 0; + + upnCur = conf; + while (upnCur != NULL) { + if (wolfSSHD_ConfigGetAuthorizedUPNDomains(upnCur) != NULL) { + #ifdef WOLFSSH_IGNORE_UNKNOWN_CONFIG + if (upnNode == 0) { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring AuthorizedUPNDomains (global " + "config): this build cannot enforce it;"); + } + else { + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] Ignoring AuthorizedUPNDomains (Match block " + "%u): this build cannot enforce it;", upnNode); + } + wolfSSH_Log(WS_LOG_WARN, + "[SSHD] it requires wolfSSL with WOLFSSL_FPKI and " + "wolfSSH with certificate support."); + #else + if (upnNode == 0) { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] AuthorizedUPNDomains is set (global config) " + "but this build cannot enforce it;"); + } + else { + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] AuthorizedUPNDomains is set (Match block %u) " + "but this build cannot enforce it;", upnNode); + } + wolfSSH_Log(WS_LOG_ERROR, + "[SSHD] it requires wolfSSL with WOLFSSL_FPKI and " + "wolfSSH with certificate support."); + ret = WS_BAD_ARGUMENT; + #endif + break; + } + upnCur = wolfSSHD_ConfigGetNext(upnCur); + upnNode++; + } + } +#endif /* !WOLFSSL_FPKI || !WOLFSSH_CERTS */ + if (ret == WS_SUCCESS) { wolfSSH_SetUserAuthTypes(*ctx, DefaultUserAuthTypes); } @@ -2499,8 +3097,10 @@ static VOID CALLBACK GraceTimeoutCb(PTP_CALLBACK_INSTANCE instance, PVOID ctx, if (conn != NULL) { /* published with an interlocked write so the connection thread reading - * the flag in LoginGraceExpired() sees it without a data race */ - InterlockedExchange8((volatile CHAR*)&conn->timeOut, 1); + * the flag in LoginGraceExpired() sees it without a data race; OR is + * used because mingw-w64 has no InterlockedExchange8, and the flag + * only ever transitions 0 -> 1 */ + InterlockedOr8((volatile CHAR*)&conn->timeOut, 1); } (void)instance; (void)timer; @@ -2524,7 +3124,7 @@ static void alarmCatch(int signum) static int LoginGraceExpired(WOLFSSHD_CONNECTION* conn) { #ifdef _WIN32 - /* interlocked read pairs with the InterlockedExchange8 in GraceTimeoutCb; + /* interlocked read pairs with the InterlockedOr8 in GraceTimeoutCb; * the OR with 0 is a read-modify-write, hence the non-const parameter */ return InterlockedOr8((volatile CHAR*)&conn->timeOut, 0); #else @@ -2620,6 +3220,9 @@ static void* HandleConnection(void* arg) { int ret = WS_SUCCESS; int error; +#ifdef _WIN32 + byte threaded = 0; +#endif WOLFSSHD_CONNECTION* conn = NULL; WOLFSSH* ssh = NULL; @@ -2671,7 +3274,7 @@ static void* HandleConnection(void* arg) if (conn->loginTimer == NULL) { /* fail closed: mark the connection as timed out so the accept * loop exits immediately rather than enforcing no grace time */ - InterlockedExchange8((volatile CHAR*)&conn->timeOut, 1); + InterlockedOr8((volatile CHAR*)&conn->timeOut, 1); wolfSSH_Log(WS_LOG_ERROR, "[SSHD] Unable to create login grace " "timer, closing connection"); } @@ -2761,6 +3364,8 @@ static void* HandleConnection(void* arg) ret = WS_FATAL_ERROR; } } + #else + WOLFSSH_UNUSED(pPasswd); #endif if (ret != WS_FATAL_ERROR) { @@ -2955,14 +3560,31 @@ static void* HandleConnection(void* arg) shutdown(conn->fd, 1); /* Spin until socket closes. */ do { - ret = (int)recv(conn->fd, sc, 1024, 0); + ret = (int)recv(conn->fd, (char*)sc, 1024, 0); } while (ret > 0); WCLOSESOCKET(conn->fd); } wolfSSH_Log(WS_LOG_INFO, "[SSHD] Return from closing connection = %d", ret); +#ifdef _WIN32 + if (conn != NULL) { + threaded = conn->isThreaded; + } +#endif WFREE(conn, NULL, DYNTYPE_SSHD); + /* The repeat state is per connection only when each connection is its own + * process (POSIX fork) or the lone in-process connection. Windows daemon + * threads share it, so flushing here would clear a streak another live + * connection thread still owns; the shutdown flush covers that path. */ +#ifdef _WIN32 + if (!threaded) { + wolfSSHDLoggingFlush(); + } +#else + wolfSSHDLoggingFlush(); +#endif + #ifdef _WIN32 return 0; #else @@ -3014,7 +3636,8 @@ static int NewConnection(WOLFSSHD_CONNECTION* conn) ret = WS_FATAL_ERROR; } else { - wolfSSH_Log(WS_LOG_INFO, "[SSHD] Spawned new thread %d\n", id); + wolfSSH_Log(WS_LOG_INFO, "[SSHD] Spawned new thread %lu\n", + (unsigned long)id); CloseHandle(t); } } @@ -3252,6 +3875,9 @@ static int StartSSHD(int argc, char** argv) ret = WFOPEN(NULL, &logFile, myoptarg, "ab"); if (ret != 0 || logFile == WBADFILE) { fprintf(stderr, "Unable to open log file %s\n", myoptarg); + /* option parsing continues and may log before the error is + * acted on, so never leave the stream NULL */ + logFile = stderr; ret = WS_FATAL_ERROR; } break; @@ -3329,8 +3955,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) { @@ -3560,6 +4198,7 @@ static int StartSSHD(int argc, char** argv) #endif } if (quit && logFile) { + wolfSSHDLoggingFlush(); fprintf(logFile, "Closing down wolfSSHD\n"); } } diff --git a/configure.ac b/configure.ac index efeb4507a..e1b3a35f7 100644 --- a/configure.ac +++ b/configure.ac @@ -294,13 +294,31 @@ 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"]) +dnl Initialize before the feature block so a stray WINCERT_LIBS value in the +dnl environment is never silently appended to LIBS for an unrelated build. +WINCERT_LIBS= 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. + dnl Accumulate the libraries in WINCERT_LIBS; they are appended to LIBS + dnl after the remaining AC_CHECK_LIB/AX_HARDEN_CC_COMPILER_FLAGS probes + dnl so a missing libncrypt cannot make those probes fail with unrelated + dnl error messages. 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*],[WINCERT_LIBS="-lcrypt32 -lncrypt"], + [AC_MSG_ERROR([--enable-windows-cert-store is only supported on _WIN32 Windows hosts (mingw)])]) + dnl Fail at configure time, not first link, when the SDK headers are + dnl absent. The link libraries are stock mingw import libs, so probing + dnl the headers is enough and keeps working without a cross-built + dnl wolfSSL installed. + AC_CHECK_HEADERS([windows.h],, + [AC_MSG_ERROR([windows.h not found; --enable-windows-cert-store needs the Windows SDK headers])]) + AC_CHECK_HEADERS([wincrypt.h ncrypt.h],, + [AC_MSG_ERROR([wincrypt.h/ncrypt.h not found; --enable-windows-cert-store needs the Windows SDK headers])], + [[#include ]])]) AS_IF([test "x$ENABLED_SMALLSTACK" = "xyes"], [AM_CPPFLAGS="$AM_CPPFLAGS -DWOLFSSH_SMALL_STACK"]) AS_IF([test "x$ENABLED_NONE_CIPHER" = "xyes"], @@ -358,7 +376,12 @@ 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/server-cert.der:keys/server-cert.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"]) @@ -379,6 +402,8 @@ AM_CONDITIONAL([BUILD_KEYBOARD_INTERACTIVE],[test "x$ENABLED_KEYBOARD_INTERACTIV AX_HARDEN_CC_COMPILER_FLAGS +AS_IF([test -n "$WINCERT_LIBS"],[LIBS="$LIBS $WINCERT_LIBS"]) + CREATE_HEX_VERSION AC_SUBST([AM_CPPFLAGS]) AC_SUBST([AM_CFLAGS]) diff --git a/examples/client/common.c b/examples/client/common.c index 164c67b46..c6d471ae1 100644 --- a/examples/client/common.c +++ b/examples/client/common.c @@ -48,17 +48,11 @@ #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; @@ -1180,18 +1174,15 @@ 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; userPublicKeyAlloc = 0; + /* The type points at a static name owned by the freed credential's + * loader; clear it with the key so nothing reads it stale. */ + userPublicKeyType = NULL; + userPublicKeyTypeSz = 0; } if (privKeyName != NULL && userPrivateKey != NULL) { @@ -1202,6 +1193,10 @@ void ClientFreeBuffers(const char* pubKeyName, const char* privKeyName, userPrivateKeyAlloc = 0; } userPrivateKeySz = 0; + /* The type points at a static name owned by the freed credential's + * loader; clear it with the key so nothing reads it stale. */ + userPrivateKeyType = NULL; + userPrivateKeyTypeSz = 0; } #ifdef WOLFSSH_KEYBOARD_INTERACTIVE @@ -1217,15 +1212,17 @@ 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; } - 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; @@ -1237,61 +1234,72 @@ 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) { - word32 i; + const char* keyType = NULL; + const byte* cert = NULL; + word32 certSz = 0; + byte* certCopy = NULL; 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); + /* 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; + * the accessor reports that slot's certificate and algorithm name. */ + if (wolfSSH_CTX_GetCertStoreCert(ctx, &cert, &certSz, &keyType) + != WS_SUCCESS || cert == NULL || certSz == 0) { + fprintf(stderr, "No cert store key with an x509v3 algorithm found in " + "CTX. In the default build RSA cert store keys carry no " + "x509v3 slot because x509v3-ssh-rsa signs with SHA-1, which " + "is soft disabled; define WOLFSSH_NO_SHA1_SOFT_DISABLE (and " + "keep WOLFSSH_NO_SSH_RSA_SHA1 undefined) to enable it, see " + "ide/winvs/user_settings.h.\n"); + return WS_BAD_ARGUMENT; + } - /* No in-memory private key — signing goes through the cert store. */ - userPrivateKey = NULL; - userPrivateKeySz = 0; + /* Copy the DER certificate before touching the globals so a failure + * leaves them alone. ClientFreeBuffers() frees the copy. */ + certCopy = (byte*)WMALLOC(certSz, heap, DYNTYPE_PRIVKEY); + if (certCopy == NULL) { + return WS_MEMORY_E; + } + WMEMCPY(certCopy, cert, certSz); - 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); } - fprintf(stderr, "No cert store key found in CTX\n"); - return WS_BAD_ARGUMENT; + userPublicKey = certCopy; + userPublicKeySz = certSz; + userPublicKeyAlloc = 1; + userPublicKeyType = (const byte*)keyType; + userPublicKeyTypeSz = (word32)WSTRLEN(keyType); + + /* No in-memory private key, signing goes through the cert store. Clear + * the alloc flag unconditionally alongside the pointer: a failed + * ClientSetPrivateKey() can leave the flag set with a NULL pointer, and + * the flag must never be set while the pointer targets the static + * buffer (ClientFreeBuffers would WFREE it). */ + userPrivateKey = userPrivateKeyBuf; + userPrivateKeySz = 0; + userPrivateKeyAlloc = 0; + /* Clear the type a superseded ClientSetPrivateKey() left behind so it + * cannot be read against the now-absent in-memory key. */ + userPrivateKeyType = NULL; + userPrivateKeyTypeSz = 0; + + pubKeyLoaded = 1; + return WS_SUCCESS; } #endif /* WOLFSSH_WINDOWS_CERT_STORE */ diff --git a/examples/client/common.h b/examples/client/common.h index ffb97638f..81ffd078c 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, @@ -38,7 +44,12 @@ int ClientSetTpm(WOLFSSH* ssh); #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); +/* Supersedes ClientUseCert()/ClientUsePubKey()/ClientSetPrivateKey(), any key + * they loaded is released. Copies the certificate out of ctx; call + * ClientFreeBuffers() to release the copy. heap must be the same heap + * previously passed to those loaders and later to ClientFreeBuffers(), + * since buffers they allocated are freed here. */ +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 5c6a62c3f..49baaa39d 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 @@ -3007,13 +3009,47 @@ 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(" flags: CURRENT_USER (default), LOCAL_MACHINE, " + "USERS,\n"); + printf(" CURRENT_SERVICE, SERVICES, " + "CURRENT_USER_GROUP_POLICY,\n"); + printf(" LOCAL_MACHINE_GROUP_POLICY, " + "LOCAL_MACHINE_ENTERPRISE,\n"); + printf(" each also with a CERT_SYSTEM_STORE_ prefix, or a " + "number\n"); + printf(" with -W 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"); } +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Detects whether argv or the environment requests a host key from the + * Windows certificate store, without doing the full option parse that + * echoserver_test() does later. Used to decide, before any file-based + * key lookup, whether the root directory search for PEM key files should + * be skipped. */ +static int EchoserverUsingCertStore(int argc, char** argv) +{ + int i; + const char* spec; + + for (i = 1; i < argc; i++) { + if (WSTRNCMP(argv[i], "-W", 2) == 0) { + return 1; + } + } + + spec = getenv("WOLFSSH_CERT_STORE"); + return (spec != NULL && spec[0] != '\0'); +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + static INLINE void SignalTcpReady(tcp_ready* ready, word16 port) { #if defined(_POSIX_THREADS) && defined(NO_MAIN_DRIVER) && \ @@ -3105,8 +3141,8 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) const char* cipherList = NULL; ES_HEAP_HINT* heap = NULL; #ifdef WOLFSSH_TPM - static char* tpmKeyPath = NULL; - static char* tpmHostKeyPath = NULL; + char* tpmKeyPath = NULL; + char* tpmHostKeyPath = NULL; #endif int multipleConnections = 1; int userEcc = 0; @@ -3136,9 +3172,6 @@ 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:DefEp:R:Ni:j:i:I:J:K:P:k:b:x:m:c:s:G:HW:"; myoptind = 0; @@ -3270,11 +3303,11 @@ THREAD_RETURN WOLFSSH_THREAD echoserver_test(void* args) useCustomHighWaterCb = 1; break; - #ifdef WOLFSSH_WINDOWS_CERT_STORE +#ifdef WOLFSSH_WINDOWS_CERT_STORE case 'W': certStoreSpec = myoptarg; break; - #endif +#endif default: ShowUsage(); @@ -3284,6 +3317,32 @@ 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; 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"); + } + } + #endif + +#if defined(WOLFSSH_TPM) && defined(WOLFSSH_WINDOWS_CERT_STORE) + /* Both register a host key on the same CTX; loading both would leave + * which key the server presents up to algorithm negotiation. The SFTP + * client and wolfsshd reject the equivalent mixes the same way. + * Checked before wc_InitMutex(&doneLock) so ES_ERROR's return path + * does not leak an initialized mutex. */ + if (tpmHostKeyPath != NULL && certStoreSpec != NULL) { + ES_ERROR("-W cannot be combined with -G\n"); + } +#endif + wc_InitMutex(&doneLock); #ifdef WOLFSSH_TEST_BLOCK @@ -3489,16 +3548,43 @@ 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 + #ifdef WOLFSSH_KEYBOARD_INTERACTIVE + if (kbAuthData.promptCount > 0) { + WFREE(kbAuthData.promptLengths, NULL, 0); + WFREE(kbAuthData.prompts, NULL, 0); + WFREE(kbAuthData.promptEcho, NULL, 0); + } + #endif + wc_FreeMutex(&doneLock); + PwMapListDelete(&pwMapList); + wolfSSH_CTX_free(ctx); 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); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); if (ret != WS_SUCCESS) { + #ifdef WOLFSSH_SMALL_STACK + wc_ForceZero(keyLoadBuf, EXAMPLE_KEYLOAD_BUFFER_SZ); + WFREE(keyLoadBuf, NULL, 0); + #endif + #ifdef WOLFSSH_KEYBOARD_INTERACTIVE + if (kbAuthData.promptCount > 0) { + WFREE(kbAuthData.promptLengths, NULL, 0); + WFREE(kbAuthData.prompts, NULL, 0); + WFREE(kbAuthData.promptEcho, NULL, 0); + } + #endif + wc_FreeMutex(&doneLock); + PwMapListDelete(&pwMapList); + wolfSSH_CTX_free(ctx); ES_ERROR("Couldn't load host key from certificate store.\n"); } loadDefaultHostKeys = 0; @@ -3929,32 +4015,18 @@ int wolfSSH_Echoserver(int argc, char** argv) #ifdef DEBUG_WOLFSSH wolfSSH_Debugging_ON(); #endif - #if !defined(WOLFSSL_NUCLEUS) && !defined(INTEGRITY) && !defined(__INTEGRITY) - { - 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; - } - } - } + /* 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 (!EchoserverUsingCertStore(argc, argv)) #endif - if (!useStore) { - ChangeToWolfSshRoot(); - } + { + ChangeToWolfSshRoot(); } #endif + #ifndef NO_WOLFSSH_SERVER echoserver_test(&args); #else diff --git a/examples/scpclient/scpclient.c b/examples/scpclient/scpclient.c index 8b817ad5f..1d9856167 100644 --- a/examples/scpclient/scpclient.c +++ b/examples/scpclient/scpclient.c @@ -334,6 +334,8 @@ THREAD_RETURN WOLFSSH_THREAD scp_client(void* args) } WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + /* release the example's own key buffers before CTX teardown */ + ClientFreeBuffers(pubKeyName, privKeyName, NULL); wolfSSH_CTX_free(ctx); if (ret != WS_SUCCESS && ret != WS_SOCKET_ERROR_E && ret != WS_CHANNEL_CLOSED) { @@ -341,7 +343,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 9bb6ab4e2..b7ae54d99 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 @@ -47,19 +49,16 @@ #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 +/* Shared by sftpclient_test() and the -W pre-scan in main(). */ +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #define SFTPC_OPTLIST_STORE "W:" +#else + #define SFTPC_OPTLIST_STORE "" +#endif +#define SFTPC_OPTLIST "?d:gh:i:j:k:l:p:r:u:EGNP:J:A:X" SFTPC_OPTLIST_STORE + #if defined(WOLFSSH_SFTP) && !defined(NO_WOLFSSH_CLIENT) /* static so that signal handler can access and interrupt get/put */ @@ -410,9 +409,22 @@ 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 server host key " + "algos to accept\n"); #ifdef WOLFSSH_WINDOWS_CERT_STORE - printf(" -W Windows cert store: \"store:subject:flags\"\n"); + printf(" -W Windows cert store: \"store:subject[:flags]\"\n"); printf(" Example: -W \"My:CN=MyCert:CURRENT_USER\"\n"); + printf(" flags: CURRENT_USER (default), LOCAL_MACHINE,\n"); + printf(" USERS, CURRENT_SERVICE, SERVICES,\n"); + printf(" CURRENT_USER_GROUP_POLICY,\n"); + printf(" LOCAL_MACHINE_GROUP_POLICY,\n"); + printf(" LOCAL_MACHINE_ENTERPRISE, each also with a\n"); + printf(" CERT_SYSTEM_STORE_ prefix, or a number.\n"); + printf(" -W supplies both keys; it can not be used with " + "-i, -j or -J.\n"); + printf(" -W also skips the wolfssh home directory search,\n"); + printf(" so -A/-d/-l resolve against the current " + "directory\n"); #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_CERTS printf(" -J filename for DER certificate to use\n"); @@ -485,24 +497,24 @@ static int sftpParseModeAndPath(char* pt, char* modeBuf, char** pathOut, sz--; } - for (idx = 0; idx < sz && pt[0] == ' '; idx++, pt++); + for (idx = 0; idx < sz && (pt[0] == ' ' || pt[0] == '\t'); idx++, pt++); sz = (word32)WSTRLEN(pt); sz = (sz < WOLFSSH_MAX_OCTET_LEN - 1) ? sz : WOLFSSH_MAX_OCTET_LEN - 1; WMEMCPY(modeBuf, pt, sz); modeBuf[sz] = '\0'; for (idx = 0; idx < sz; idx++) { - if (modeBuf[idx] == ' ') { + if (modeBuf[idx] == ' ' || modeBuf[idx] == '\t') { modeBuf[idx] = '\0'; break; } } - if (idx == 0 || (idx == sz && pt[sz] != ' ')) + if (idx == 0 || (idx == sz && pt[sz] != ' ' && pt[sz] != '\t')) return 1; pt += (word32)WSTRLEN(modeBuf); sz = (word32)WSTRLEN(pt); - for (idx = 0; idx < sz && pt[0] == ' '; idx++, pt++); + for (idx = 0; idx < sz && (pt[0] == ' ' || pt[0] == '\t'); idx++, pt++); if (pt[0] == '\0') return 1; @@ -560,6 +572,100 @@ static int doCmds(func_args* args) } msg[WOLFSSH_MAX_FILENAME * 2 - 1] = '\0'; + /* Anchored to the start of the line so a file name containing + * "creat" in another command does not match, and dispatched ahead + * of the unanchored substring matchers below so none of them can + * steal a creat line whose mode or path contains "get", "cd", etc. + * The keyword may be followed by a space, tab, or end of line; the + * bare-keyword form still reaches sftpParseModeAndPath so its + * empty-argument handling stays covered. */ + pt = msg; + while (*pt == ' ' || *pt == '\t') + pt++; + if (WSTRNCMP(pt, "creat", 5) == 0 && + (pt[5] == '\0' || pt[5] == ' ' || pt[5] == '\t' || + pt[5] == '\n')) { + char* f = NULL; + char* path; + char mode[WOLFSSH_MAX_OCTET_LEN]; + byte handle[WOLFSSH_MAX_HANDLE]; + word32 handleSz = WOLFSSH_MAX_HANDLE; + WS_SFTP_FILEATRB atr; + int parseRet; + int openRet = WS_FATAL_ERROR; + unsigned long perVal; + char* modeEnd; + + pt += 5; + while (*pt == ' ' || *pt == '\t') + pt++; + parseRet = sftpParseModeAndPath(pt, mode, &path, &f, workingDir); + if (parseRet == 1) { + printf("error with getting mode\r\n"); + continue; + } + if (parseRet == -1) { + err_msg("Error malloc'ing"); + return -1; + } + + /* build permission attribute from octal mode string; + * wolfSSH_oct2dec is internal scope so strtoul is used here */ + perVal = strtoul(mode, &modeEnd, 8); + if (*modeEnd == '\0' && perVal <= 07777) { + WMEMSET(&atr, 0, sizeof(WS_SFTP_FILEATRB)); + atr.flags = WOLFSSH_FILEATRB_PERM; + atr.per = (word32)perVal; + + /* open (create) remote file with the given permissions */ + handleSz = WOLFSSH_MAX_HANDLE; + do { + while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { + ret = wolfSSH_get_error(ssh); + } + } + ret = wolfSSH_SFTP_Open(ssh, path, + WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | + WOLFSSH_FXF_TRUNC, &atr, handle, &handleSz); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE || + err == WS_REKEYING) && ret != WS_SUCCESS); + openRet = ret; + } + if (openRet == WS_SUCCESS) { + do { + while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { + ret = wolfSSH_worker(ssh, NULL); + if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { + ret = wolfSSH_get_error(ssh); + } + } + ret = wolfSSH_SFTP_Close(ssh, handle, handleSz); + err = wolfSSH_get_error(ssh); + } while ((err == WS_WANT_READ || err == WS_WANT_WRITE || + err == WS_REKEYING) && ret != WS_SUCCESS); + if (ret != WS_SUCCESS) { + if (SFTP_FPUTS(args, "Unable to close file handle\n") < 0) { + err_msg("fputs error"); + WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return -1; + } + } + } + else { + if (SFTP_FPUTS(args, "Unable to create file\n") < 0) { + err_msg("fputs error"); + WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); + return -1; + } + } + + WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); + continue; + } + if ((pt = WSTRNSTR(msg, "mkdir", sizeof(msg))) != NULL) { WS_SFTP_FILEATRB atrb; int sz; @@ -967,86 +1073,6 @@ static int doCmds(func_args* args) continue; } - if ((pt = WSTRNSTR(msg, "creat", MAX_CMD_SZ)) != NULL) { - char* f = NULL; - char* path; - char mode[WOLFSSH_MAX_OCTET_LEN]; - byte handle[WOLFSSH_MAX_HANDLE]; - word32 handleSz = WOLFSSH_MAX_HANDLE; - WS_SFTP_FILEATRB atr; - int parseRet; - int openRet = WS_FATAL_ERROR; - unsigned long perVal; - char* modeEnd; - - pt += sizeof("creat"); - parseRet = sftpParseModeAndPath(pt, mode, &path, &f, workingDir); - if (parseRet == 1) { - printf("error with getting mode\r\n"); - continue; - } - if (parseRet == -1) { - err_msg("Error malloc'ing"); - return -1; - } - - /* build permission attribute from octal mode string; - * wolfSSH_oct2dec is internal scope so strtoul is used here */ - perVal = strtoul(mode, &modeEnd, 8); - if (*modeEnd == '\0' && perVal <= 07777) { - WMEMSET(&atr, 0, sizeof(WS_SFTP_FILEATRB)); - atr.flags = WOLFSSH_FILEATRB_PERM; - atr.per = (word32)perVal; - - /* open (create) remote file with the given permissions */ - handleSz = WOLFSSH_MAX_HANDLE; - do { - while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { - ret = wolfSSH_worker(ssh, NULL); - if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { - ret = wolfSSH_get_error(ssh); - } - } - ret = wolfSSH_SFTP_Open(ssh, path, - WOLFSSH_FXF_WRITE | WOLFSSH_FXF_CREAT | - WOLFSSH_FXF_TRUNC, &atr, handle, &handleSz); - err = wolfSSH_get_error(ssh); - } while ((err == WS_WANT_READ || err == WS_WANT_WRITE || - err == WS_REKEYING) && ret != WS_SUCCESS); - openRet = ret; - } - if (openRet == WS_SUCCESS) { - do { - while (ret == WS_REKEYING || ssh->error == WS_REKEYING) { - ret = wolfSSH_worker(ssh, NULL); - if (ret != WS_SUCCESS && ret == WS_FATAL_ERROR) { - ret = wolfSSH_get_error(ssh); - } - } - ret = wolfSSH_SFTP_Close(ssh, handle, handleSz); - err = wolfSSH_get_error(ssh); - } while ((err == WS_WANT_READ || err == WS_WANT_WRITE || - err == WS_REKEYING) && ret != WS_SUCCESS); - if (ret != WS_SUCCESS) { - if (SFTP_FPUTS(args, "Unable to close file handle\n") < 0) { - err_msg("fputs error"); - WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return -1; - } - } - } - else { - if (SFTP_FPUTS(args, "Unable to create file\n") < 0) { - err_msg("fputs error"); - WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); - return -1; - } - } - - WFREE(f, NULL, DYNAMIC_TYPE_TMP_BUFFER); - continue; - } - if ((pt = WSTRNSTR(msg, "rmdir", MAX_CMD_SZ)) != NULL) { int sz; char* f = NULL; @@ -1582,6 +1608,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,11 +1618,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" -#ifdef WOLFSSH_WINDOWS_CERT_STORE - "W:" -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ - )) != -1) { + while ((ch = mygetopt(argc, argv, SFTPC_OPTLIST)) != -1) { switch (ch) { case 'd': defaultSftpPath = myoptarg; @@ -1657,6 +1680,10 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) pubKeyName = myoptarg; break; + case 'k': + keyList = myoptarg; + break; + #ifdef WOLFSSH_CERTS case 'J': certName = myoptarg; @@ -1693,6 +1720,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."); } @@ -1733,7 +1768,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"); } @@ -1741,32 +1776,31 @@ 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); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); 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); + wolfSSH_FreeCertStoreSpec(wStoreName, wSubjectName, heap); + wStoreName = NULL; + wSubjectName = NULL; if (ret != WS_SUCCESS) { - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + wolfSSH_CTX_free(ctx); 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, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); + wolfSSH_CTX_free(ctx); err_sys("Error setting up cert store auth"); } - - WFREE(wStoreName, NULL, DYNTYPE_TEMP); - WFREE(wSubjectName, NULL, DYNTYPE_TEMP); } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { @@ -1794,6 +1828,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 @@ -1851,8 +1891,16 @@ 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) { + /* the return is a generic failure code; the real cause is kept in + * the session error */ + int err; + + err = wolfSSH_get_error(ssh); + fprintf(stderr, "wolfSSH_SFTP_connect failed: %d, %s\n", err, + wolfSSH_ErrorToName(err)); err_sys("Couldn't connect SFTP"); + } { /* get current working directory */ @@ -1936,13 +1984,17 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) WCLOSESOCKET(sockFd); wolfSSH_free(ssh); + + /* ClientFreeBuffers() releases the certificate copy + * ClientSetupCertStoreAuth() made; it must use the same heap that was + * passed there. */ + 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 @@ -1997,7 +2049,30 @@ THREAD_RETURN WOLFSSH_THREAD sftpclient_test(void* args) wolfSSH_Init(); - ChangeToWolfSshRoot(); + { + int useStore = 0; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + { + int ch; + + /* With -W the keys come from the Windows certificate store + * and no file-based keys are needed, so skip the root + * directory search. Parse rather than match on argv, an + * option value could start with "-W". */ + myoptind = 0; + while ((ch = mygetopt(argc, argv, SFTPC_OPTLIST)) != -1) { + if (ch == 'W') { + useStore = 1; + break; + } + } + myoptind = 0; + } + #endif + if (!useStore) { + ChangeToWolfSshRoot(); + } + } sftpclient_test(&args); wolfSSH_Cleanup(); diff --git a/ide/winvs/README.md b/ide/winvs/README.md index 856bf8fff..6611d5c7f 100644 --- a/ide/winvs/README.md +++ b/ide/winvs/README.md @@ -6,6 +6,11 @@ example and test programs. The solution provides both Debug and Release builds of Static and Dynamic 32- or 64-bit libraries. The file `user_settings.h` should be used in the wolfSSL build to configure it. +The projects link against the Windows `crypt32.lib` and `ncrypt.lib` +import libraries for the MS Certificate Store support +(`WOLFSSH_WINDOWS_CERT_STORE`; see the comment block in +`user_settings.h`). + This project assumes that the wolfSSH and wolfSSL source directories are installed side-by-side and do not have the version number in their diff --git a/ide/winvs/api-test/api-test.vcxproj b/ide/winvs/api-test/api-test.vcxproj index 2524860b7..8b40665e9 100644 --- a/ide/winvs/api-test/api-test.vcxproj +++ b/ide/winvs/api-test/api-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -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/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 cf1e70a18..f716a0433 100644 --- a/ide/winvs/unit-test/unit-test.vcxproj +++ b/ide/winvs/unit-test/unit-test.vcxproj @@ -1,4 +1,4 @@ - + @@ -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/user_settings.h b/ide/winvs/user_settings.h index d923629bf..dba3b77f9 100644 --- a/ide/winvs/user_settings.h +++ b/ide/winvs/user_settings.h @@ -58,6 +58,28 @@ #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 a disabled preprocessor block so enabling + * the X.509 block above does not silently pull it in (and so CI's global + * block-enabling sed cannot flip it either). + * + * #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. + * + * The store lookup rejects a CN match whose certificate is expired or not + * yet valid when no time-valid one exists; define + * #define WOLFSSH_CERT_STORE_ALLOW_EXPIRED + * to select such a certificate anyway (the fallback is logged). + */ + /* default SSHD options */ #if 0 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..86a146b67 100644 --- a/src/certman.c +++ b/src/certman.c @@ -36,6 +36,7 @@ #endif +#include #include #include #include @@ -45,14 +46,40 @@ #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_LOCATION_SHIFT + #define CERT_SYSTEM_STORE_LOCATION_SHIFT 16 + #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 + #ifndef CERT_SYSTEM_STORE_CURRENT_SERVICE + #define CERT_SYSTEM_STORE_CURRENT_SERVICE 0x00040000 + #endif + #ifndef CERT_SYSTEM_STORE_SERVICES + #define CERT_SYSTEM_STORE_SERVICES 0x00050000 + #endif + #ifndef CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY + #define CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY 0x00070000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY 0x00080000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE 0x00090000 + #endif #endif #ifdef WOLFSSH_CERTS @@ -99,27 +126,55 @@ struct WOLFSSH_CERTMAN { */ 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; } - /* importing the manager already in use is a no-op */ - if (ctx->certMan->cm == cm) { - return WS_SUCCESS; + /* importing the manager already in use is a no-op beyond the policy; + * the OCSP policy below is still (re)applied so the documented side + * effect holds even when the same manager is imported twice */ + if (ctx->certMan->cm != cm) { + /* Take the reference before mutating the caller's manager so a + * WS_FATAL_ERROR return always means nothing changed: on an OCSP + * policy failure below the reference is released again and the + * caller's manager keeps its previous policy. */ + if (wolfSSL_CertManager_up_ref(cm) != WOLFSSL_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Failed to increment cert manager reference"); + return WS_FATAL_ERROR; + } } - if (wolfSSL_CertManager_up_ref(cm) != WOLFSSL_SUCCESS) { - WLOG(WS_LOG_CERTMAN, "Failed to increment cert manager reference"); +#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"); + if (ctx->certMan->cm != cm) { + /* drop the reference taken above */ + wolfSSL_CertManagerFree(cm); + } return WS_FATAL_ERROR; } +#endif - /* free up existing cm if present */ - if (ctx->certMan->cm != NULL) { - wolfSSL_CertManagerFree(ctx->certMan->cm); + if (ctx->certMan->cm != cm) { + /* free up existing cm if present */ + if (ctx->certMan->cm != NULL) { + wolfSSL_CertManagerFree(ctx->certMan->cm); + } + ctx->certMan->cm = cm; } - ctx->certMan->cm = cm; return WS_SUCCESS; +#endif } @@ -684,9 +739,112 @@ 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. +/* 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); +} + + +/* The one name-to-value table for CERT_SYSTEM_STORE_* locations, shared by + * wolfSSH_ParseCertStoreSpec() and wolfsshd's HostKeyStoreFlags and + * wolfSSH_WinUserDwFlags parsing so the accepted spellings cannot drift. */ +static const struct { + const char* shortName; + const char* longName; + word32 value; +} certStoreLocations[] = { + { "CURRENT_USER", "CERT_SYSTEM_STORE_CURRENT_USER", + (word32)CERT_SYSTEM_STORE_CURRENT_USER }, + { "LOCAL_MACHINE", "CERT_SYSTEM_STORE_LOCAL_MACHINE", + (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE }, + { "USERS", "CERT_SYSTEM_STORE_USERS", + (word32)CERT_SYSTEM_STORE_USERS }, + { "CURRENT_SERVICE", "CERT_SYSTEM_STORE_CURRENT_SERVICE", + (word32)CERT_SYSTEM_STORE_CURRENT_SERVICE }, + { "SERVICES", "CERT_SYSTEM_STORE_SERVICES", + (word32)CERT_SYSTEM_STORE_SERVICES }, + { "CURRENT_USER_GROUP_POLICY", + "CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY", + (word32)CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY }, + { "LOCAL_MACHINE_GROUP_POLICY", + "CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY", + (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY }, + { "LOCAL_MACHINE_ENTERPRISE", + "CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE", + (word32)CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE }, +}; + + +/* Parse a Windows system-store location, given as a CERT_SYSTEM_STORE_* name + * (long or short form) or as a decimal or 0x-prefixed hex number, so both + * 65536 and 0x00010000 work; a leading zero is not reinterpreted as octal, + * so "0262144" parses as decimal 262144. The number must be consumed whole + * and start with a digit: strtoul()'s leading whitespace and sign handling + * is rejected so a config typo such as "-0xFFFF0000" cannot wrap to a valid + * location. 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. */ +int wolfSSH_CertStoreLocationFromName(const char* in, word32* out) +{ + int ret = WS_BAD_ARGUMENT; + word32 i; + unsigned long val; + char* end; + + if (in == NULL || out == NULL || *in == '\0') { + return WS_BAD_ARGUMENT; + } + + for (i = 0; i < (word32)(sizeof(certStoreLocations) / + sizeof(*certStoreLocations)); i++) { + if (WSTRCMP(in, certStoreLocations[i].shortName) == 0 || + WSTRCMP(in, certStoreLocations[i].longName) == 0) { + *out = certStoreLocations[i].value; + ret = WS_SUCCESS; + break; + } + } + + if (ret != WS_SUCCESS && *in >= '0' && *in <= '9') { + int base = 10; + + if (in[0] == '0' && (in[1] == 'x' || in[1] == 'X')) { + base = 16; + } + + end = NULL; + errno = 0; + val = strtoul(in, &end, base); + if (end != in && *end == '\0' && errno != ERANGE && + wolfSSH_CertStoreLocationValid((word32)val) && + val == (unsigned long)(word32)val) { + *out = (word32)val; + ret = WS_SUCCESS; + } + } + + return ret; +} + + +/* 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; + * 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, @@ -696,17 +854,27 @@ int wolfSSH_ParseCertStoreSpec(const char* spec, char* storeName = NULL; char* subjectName = NULL; char* flagsStr = NULL; + word32 flags; int wStoreNameLen, wSubjectNameLen; size_t specLen; - if (spec == NULL || wStoreName == NULL || wSubjectName == NULL || - dwFlags == NULL) { + /* NULL every supplied out-pointer before any failure return, including + * the argument rejections below, so the documented "out-pointers are + * NULL on failure" contract holds even when only one argument is bad. */ + if (wStoreName != NULL) { + *wStoreName = NULL; + } + if (wSubjectName != NULL) { + *wSubjectName = NULL; + } + if (wStoreName == NULL || wSubjectName == NULL || dwFlags == NULL) { + return WS_BAD_ARGUMENT; + } + if (spec == NULL) { return WS_BAD_ARGUMENT; } - *wStoreName = NULL; - *wSubjectName = NULL; - *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; + flags = CERT_SYSTEM_STORE_CURRENT_USER; specLen = WSTRLEN(spec) + 1; specCopy = (char*)WMALLOC(specLen, heap, DYNTYPE_TEMP); @@ -723,40 +891,52 @@ 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 (WSTRCMP(flagsStr, "CURRENT_USER") == 0) { - *dwFlags = CERT_SYSTEM_STORE_CURRENT_USER; - } - else if (WSTRCMP(flagsStr, "LOCAL_MACHINE") == 0) { - *dwFlags = CERT_SYSTEM_STORE_LOCAL_MACHINE; + if (WSTRCHR(flagsStr, ':') != NULL) { + WLOG(WS_LOG_CERTMAN, + "Cert store spec has too many ':'-separated fields; " + "expected store:subject[:flags]"); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; } - 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; - } + /* Accept the same spellings as wolfsshd's HostKeyStoreFlags and + * wolfSSH_WinUserDwFlags so one name works everywhere; the + * shared parser also handles the numeric location forms and + * rejects control flags. */ + if (wolfSSH_CertStoreLocationFromName(flagsStr, &flags) + != WS_SUCCESS) { + WLOG(WS_LOG_CERTMAN, "Malformed cert store flags value " + "'%s'; expected store:subject[:flags] with a " + "CERT_SYSTEM_STORE_* name or store location number", + flagsStr); + WFREE(specCopy, heap, DYNTYPE_TEMP); + return WS_BAD_ARGUMENT; } } } - if (storeName == NULL || subjectName == NULL || *storeName == '\0' || + if (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); + /* Convert to wide strings. MB_ERR_INVALID_CHARS makes a non-UTF-8 byte + * sequence fail here instead of being silently replaced with U+FFFD and + * then never matching any certificate CN. */ + wStoreNameLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + storeName, -1, NULL, 0); + wSubjectNameLen = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, + subjectName, -1, NULL, 0); if (wStoreNameLen == 0 || wSubjectNameLen == 0) { + WLOG(WS_LOG_CERTMAN, + "Cert store spec is not valid UTF-8"); WFREE(specCopy, heap, DYNTYPE_TEMP); return WS_FATAL_ERROR; } @@ -779,14 +959,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, MB_ERR_INVALID_CHARS, storeName, -1, + *wStoreName, wStoreNameLen) == 0 || + MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, 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 b53304465..5cfddb7bc 100644 --- a/src/internal.c +++ b/src/internal.c @@ -25,6 +25,23 @@ */ +/* SignWithCertStoreKey() uses Vista-only CNG/NCrypt declarations + * (HCRYPTPROV_OR_NCRYPT_KEY_HANDLE, NCryptSignHash); mingw-w64 has + * historically defaulted _WIN32_WINNT to pre-Vista, so raise the floor + * before the first header that pulls in (wolfssh/ssh.h via + * port.h does). Same rules as the pin in src/ssh.c: the undefined case is + * raised only under mingw, since MSVC's SDK defaults an undefined + * _WIN32_WINNT to its newest profile, and WINVER is pinned alongside so + * the two cannot disagree. */ +#if defined(_WIN32) && \ + ((defined(__MINGW32__) && !defined(_WIN32_WINNT)) || \ + (defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600)) + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0600 + #undef WINVER + #define WINVER 0x0600 +#endif + #ifdef HAVE_CONFIG_H #include #endif @@ -84,21 +101,22 @@ #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 + /* Fallbacks for SDKs that predate these wincrypt.h/ncrypt.h + * definitions. The values must match the SDK headers exactly. */ #ifndef CERT_NCRYPT_KEY_SPEC - #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF + #endif + #ifndef CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + #define CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG 0x00040000 #endif #ifndef BCRYPT_PAD_PKCS1 #define BCRYPT_PAD_PKCS1 0x00000002 #endif +#if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) static int ExtractPubKeyDerFromCert(const byte* certDer, word32 certDerSz, byte** outDer, word32* outDerSz, void* heap); +#endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef NO_INLINE @@ -1116,6 +1134,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 */ @@ -1217,6 +1241,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 */ @@ -1330,36 +1360,51 @@ WOLFSSH_CTX* CtxInit(WOLFSSH_CTX* ctx, byte side, void* heap) #ifdef WOLFSSH_WINDOWS_CERT_STORE +/* 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; +} + + /* 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; + } + + /* Deliberately keyed on useCertStore alone, wider than IsCertStoreKey(): + * a slot in a broken half-store state (flag set, context or key state + * inconsistent) still gets its store resources released here even though + * the signing paths would no longer treat it as store backed. */ + if (!IsCertStoreKey(pvtKey)) { + WLOG(WS_LOG_DEBUG, "ClearCertStoreKey: releasing a slot with " + "inconsistent cert-store state"); + } + 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; +#ifdef WOLFSSH_CERTS + if (pvtKey->cert != NULL) { + WFREE(pvtKey->cert, ctx->heap, DYNTYPE_CERT); + pvtKey->cert = NULL; + pvtKey->certSz = 0; } +#endif 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 */ @@ -2373,16 +2418,67 @@ static int IdentifyCertKey(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) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no certificate", IdToName(key->publicKeyFmt)); + 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 + ) { + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: skipping %s, " + "no signing source", IdToName(key->publicKeyFmt)); + continue; + } if (key->publicKeyFmt == ID_SSH_RSA) { #ifndef WOLFSSH_NO_RSA_SHA2_512 if (publicKeyAlgoCount < WOLFSSH_MAX_PUB_KEY_ALGO) { @@ -2416,6 +2512,13 @@ void RefreshPublicKeyAlgo(WOLFSSH_CTX* ctx) } } } + if (publicKeyAlgoCount == 0 && keyCount > 0) { + /* DEBUG, not ERROR: this state is transient on the documented + * "certificate first, then private key" load order and only becomes + * a failure if it persists to SendKexInit, which rejects it there. */ + WLOG(WS_LOG_DEBUG, "RefreshPublicKeyAlgo: No usable host key; every " + "loaded slot lacks a certificate or signing source"); + } ctx->publicKeyAlgoCount = publicKeyAlgoCount; } @@ -2533,14 +2636,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); @@ -2549,14 +2658,14 @@ 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 - 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) { @@ -2570,7 +2679,24 @@ static int UpdateHostCertificates(WOLFSSH_CTX* ctx, ctx->privateKey[certHint].keySz); WFREE(ctx->privateKey[certHint].key, ctx->heap, DYNTYPE_PRIVKEY); + ctx->privateKey[certHint].key = NULL; + ctx->privateKey[certHint].keySz = 0; + } + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Defensive only: SetHostPrivateKey() and + * SetHostCertificate() both reject mixing a file credential + * with a cert-store slot before reaching here, so this can + * only clear a state no writer currently produces. 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_DEBUG, "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; ctx->privateKey[certHint].keySz = keySz; #ifdef WOLFSSH_TPM @@ -2613,9 +2739,24 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, destIdx = HINTISSET(certIdx) ? certIdx : ctx->privateKeyCount; 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; @@ -2626,6 +2767,13 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, WFREE(der, ctx->heap, dynamicType); } else { + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* 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 + if (pvtKey->publicKeyFmt == certId) { if (pvtKey->cert != NULL) { WFREE(pvtKey->cert, ctx->heap, dynamicType); @@ -2636,13 +2784,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; RefreshPublicKeyAlgo(ctx); @@ -2657,6 +2798,54 @@ static int SetHostCertificate(WOLFSSH_CTX* ctx, #endif +#if defined(WOLFSSH_WINDOWS_CERT_STORE) && defined(WOLFSSH_CERTS) +/* Index of the claimed slot holding fmt, or WOLFSSH_MAX_PVT_KEYS. Shared + * with the cert-store slot bookkeeping in src/ssh.c so the two lookups + * cannot drift. */ +WOLFSSH_LOCAL word32 FindPvtKeyIdx(const WOLFSSH_CTX* ctx, byte fmt) +{ + word32 i; + + for (i = 0; i < ctx->privateKeyCount; i++) { + if (ctx->privateKey[i].publicKeyFmt == fmt) { + return i; + } + } + + return WOLFSSH_MAX_PVT_KEYS; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE && WOLFSSH_CERTS */ + + +#ifdef WOLFSSH_WINDOWS_CERT_STORE +/* True when loading a key for keyId over the slot at destIdx would clobber + * a cert-store-backed credential: either the slot itself or the x509v3 + * certificate slot paired with keyId. Shared by SetHostPrivateKey() and + * wolfSSH_SetHostTpmKey() so the two rejections cannot drift. */ +static int CertStoreSlotConflict(const WOLFSSH_CTX* ctx, word32 destIdx, + byte keyId) +{ + int conflict; + + conflict = IsCertStoreKey(ctx->privateKey + destIdx); +#ifdef WOLFSSH_CERTS + if (!conflict) { + word32 certIdx; + + certIdx = FindPvtKeyIdx(ctx, CertTypeForId(keyId)); + if (certIdx < WOLFSSH_MAX_PVT_KEYS) { + conflict = IsCertStoreKey(ctx->privateKey + certIdx); + } + } +#else + WOLFSSH_UNUSED(keyId); +#endif + + return conflict; +} +#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + + static int SetHostPrivateKey(WOLFSSH_CTX* ctx, byte keyId, byte* der, word32 derSz, int dynamicType) { @@ -2678,6 +2867,21 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, WFREE(der, ctx->heap, dynamicType); ret = WS_CTX_KEY_COUNT_E; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Mirror of SetHostCertificate()'s rejection so the two load orders + * agree: a file key over a cert-store slot would silently tear down the + * store's x509v3 slot and its certificate while still reporting + * WS_SUCCESS. Refuse the mixed configuration instead; clear the store + * key first if replacing it is intended. */ + else if (CertStoreSlotConflict(ctx, destIdx, keyId)) { + WLOG(WS_LOG_ERROR, "SetHostPrivateKey: The host key for this " + "algorithm comes from the certificate store; do not also load " + "a host key file"); + WS_FORCEZERO(der, derSz); + WFREE(der, ctx->heap, dynamicType); + ret = WS_BAD_ARGUMENT; + } +#endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; @@ -2685,6 +2889,8 @@ static int SetHostPrivateKey(WOLFSSH_CTX* ctx, if (pvtKey->key != NULL) { WS_FORCEZERO(pvtKey->key, pvtKey->keySz); WFREE(pvtKey->key, ctx->heap, dynamicType); + pvtKey->key = NULL; + pvtKey->keySz = 0; } } else { @@ -2692,13 +2898,6 @@ 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 @@ -2740,6 +2939,19 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) if (destIdx >= WOLFSSH_MAX_PVT_KEYS) { ret = WS_CTX_KEY_COUNT_E; } +#ifdef WOLFSSH_WINDOWS_CERT_STORE + /* Mirror of SetHostPrivateKey()/SetHostCertificate()'s rejection: a TPM + * key over a cert-store slot would silently tear down the store's + * x509v3 slot and its certificate while still reporting WS_SUCCESS. + * Refuse the mixed configuration instead; clear the store key first if + * replacing it is intended. */ + else if (CertStoreSlotConflict(ctx, destIdx, keyId)) { + WLOG(WS_LOG_ERROR, "wolfSSH_SetHostTpmKey: The host key for this " + "algorithm comes from the certificate store; do not also " + "register a TPM key for it"); + ret = WS_BAD_ARGUMENT; + } +#endif else { WOLFSSH_PVT_KEY* pvtKey = ctx->privateKey + destIdx; @@ -2755,6 +2967,12 @@ int wolfSSH_SetHostTpmKey(WOLFSSH_CTX* ctx, byte keyId) pvtKey->key = NULL; pvtKey->keySz = 0; pvtKey->isTpm = 1; + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* 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 #ifdef WOLFSSH_CERTS /* Mark the matching certificate slot TPM-backed so certificate KEX @@ -2771,6 +2989,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; } } @@ -5162,6 +5383,10 @@ int wcPrimeForId(byte id) case ID_ECDSA_SHA2_NISTP256: return ECC_SECP256R1; #endif +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) + case ID_X509V3_ECDSA_SHA2_NISTP256: + return ECC_SECP256R1; +#endif #ifndef WOLFSSH_NO_ECDH_SHA2_NISTP384 case ID_ECDH_SHA2_NISTP384: return ECC_SECP384R1; @@ -5174,6 +5399,10 @@ int wcPrimeForId(byte id) case ID_ECDSA_SHA2_NISTP384: return ECC_SECP384R1; #endif +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) + case ID_X509V3_ECDSA_SHA2_NISTP384: + return ECC_SECP384R1; +#endif #ifndef WOLFSSH_NO_CURVE25519_MLKEM768_SHA256 case ID_CURVE25519_MLKEM768_SHA256: return ECC_X25519; @@ -5190,6 +5419,10 @@ int wcPrimeForId(byte id) #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP521 case ID_ECDSA_SHA2_NISTP521: return ECC_SECP521R1; +#endif +#if defined(WOLFSSH_CERTS) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP521) + case ID_X509V3_ECDSA_SHA2_NISTP521: + return ECC_SECP521R1; #endif default: return ECC_CURVE_INVALID; @@ -6494,8 +6727,20 @@ static int ParseECCPubKeyCert(WOLFSSH *ssh, #ifndef WOLFSSH_NO_ECDSA byte* der = NULL; word32 derSz, idx = 0; + int expectedCurve; + int actualCurve; int error; + /* One id-to-curve map for plain and certificate keys: wcPrimeForId() + * covers the ID_X509V3_ECDSA_* ids. An unhandled id is rejected before + * the decode, matching ParseECCPubKey(), so the ECC_CURVE_INVALID + * sentinel can never compare equal in the binding check below. */ + expectedCurve = wcPrimeForId(ssh->handshake->pubKeyId); + if (expectedCurve == ECC_CURVE_INVALID) { + /* Same code ParseECCPubKey() reports for an unmapped id. */ + return WS_INVALID_PRIME_CURVE; + } + ret = ParsePubKeyCert(ssh, pubKey, pubKeySz, &der, &derSz); if (ret == WS_SUCCESS) { error = InitPubKey(sigKeyBlock_ptr, ssh); @@ -6506,6 +6751,24 @@ 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. Prefer the decoded domain + * parameters over the ecc_sets table index so a key that did not + * resolve to a table entry still binds correctly. */ + if (error == 0) { + if (sigKeyBlock_ptr->sk.ecc.key.dp != NULL) { + actualCurve = sigKeyBlock_ptr->sk.ecc.key.dp->id; + } + else { + actualCurve = + wc_ecc_get_curve_id(sigKeyBlock_ptr->sk.ecc.key.idx); + } + if (actualCurve != 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); } @@ -9443,6 +9706,29 @@ static int DoUserAuthRequestEccCert(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, ret = WS_CRYPTO_FAILED; } + /* Bind the certificate's key to the declared algorithm so one credential + * cannot authenticate under multiple x509v3-ecdsa-* names, matching the + * ParseECCPubKeyCert() binding on the host-key path. */ + if (ret == WS_SUCCESS) { + int expectedCurve; + int actualCurve; + + expectedCurve = wcPrimeForId(NameToId( + (const char*)pk->publicKeyType, pk->publicKeyTypeSz)); + if (key_ptr->dp != NULL) { + actualCurve = key_ptr->dp->id; + } + else { + actualCurve = wc_ecc_get_curve_id(key_ptr->idx); + } + if (expectedCurve == ECC_CURVE_INVALID || + actualCurve != expectedCurve) { + WLOG(WS_LOG_DEBUG, "DUAREC: certificate key curve does not " + "match the declared algorithm"); + ret = WS_INVALID_PRIME_CURVE; + } + } + if (ret == WS_SUCCESS) { i = 0; /* First check that the signature's public key type matches the one @@ -10836,8 +11122,19 @@ static int DoGlobalRequestFwd(WOLFSSH* ssh, } } else { - WLOG(WS_LOG_WARN, "No forwarding callback set, rejecting request. " - "Set one with wolfSSH_CTX_SetFwdCb()."); + /* States a fixed property of the app's configuration but fires + * per peer request: WARN once per session so the operator error + * stays visible in release builds, DEBUG for the repeats so a + * peer cannot grow the log with every tcpip-forward request. */ + if (!ssh->fwdCbMissingWarned) { + ssh->fwdCbMissingWarned = 1; + WLOG(WS_LOG_WARN, "No forwarding callback set, rejecting " + "request. Set one with wolfSSH_CTX_SetFwdCb()."); + } + else { + WLOG(WS_LOG_DEBUG, "No forwarding callback set, rejecting " + "request. Set one with wolfSSH_CTX_SetFwdCb()."); + } ret = WS_UNIMPLEMENTED_E; } } @@ -11104,7 +11401,10 @@ static int DoChannelOpen(WOLFSSH* ssh, * forward. */ if (typeId == ID_CHANTYPE_TCPIP_FORWARD && ssh->ctx->side == WOLFSSH_ENDPOINT_SERVER) { - WLOG(WS_LOG_WARN, "Rejecting forwarded-tcpip channel open " + /* Fires per peer open request, so DEBUG: at WARN an app + * that logs warnings unconditionally (e.g. wolfsshd) would + * let a peer grow the log with every open. */ + WLOG(WS_LOG_DEBUG, "Rejecting forwarded-tcpip channel open " "received by a server (wrong direction)"); fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED; ret = WS_ERROR; @@ -11116,11 +11416,27 @@ static int DoChannelOpen(WOLFSSH* ssh, ssh->channelOpenCtx); } else { - WLOG(WS_LOG_WARN, "No channel open callback set " - "(call wolfSSH_CTX_SetChannelOpenCb()), accepting " - "channel open by default; typeId=%u, " - "peerChannelId=%u", - (word32)typeId, peerChannelId); + /* Fires per channel open and announces a fail-open + * policy, so WARN once per session (visible in release + * builds) and DEBUG for the repeats, so a peer cannot + * grow the log with every open request. The + * accept-by-default policy itself is documented at + * wolfSSH_CTX_SetChannelOpenCb() in ssh.h. */ + if (!ssh->chanOpenCbMissingWarned) { + ssh->chanOpenCbMissingWarned = 1; + WLOG(WS_LOG_WARN, "No channel open callback set " + "(call wolfSSH_CTX_SetChannelOpenCb()), " + "accepting channel open by default; " + "typeId=%u, peerChannelId=%u", + (word32)typeId, peerChannelId); + } + else { + WLOG(WS_LOG_DEBUG, "No channel open callback set " + "(call wolfSSH_CTX_SetChannelOpenCb()), " + "accepting channel open by default; " + "typeId=%u, peerChannelId=%u", + (word32)typeId, peerChannelId); + } } if (ssh->channelListSz == 0) ssh->defaultPeerChannelId = peerChannelId; @@ -11143,9 +11459,20 @@ static int DoChannelOpen(WOLFSSH* ssh, else { /* Both forwarding channel types require an explicit policy * callback; without one, fail closed rather than letting - * the default-accept channelOpenCb path admit them. */ - WLOG(WS_LOG_WARN, "No forward callback set for forwarding " - "channel, failing channel open"); + * the default-accept channelOpenCb path admit them. + * Fires per peer open request: WARN once per session so + * the operator error stays visible in release builds, + * DEBUG for the repeats so a peer cannot grow the log + * with every open request. */ + if (!ssh->fwdCbMissingWarned) { + ssh->fwdCbMissingWarned = 1; + WLOG(WS_LOG_WARN, "No forward callback set for " + "forwarding channel, failing channel open"); + } + else { + WLOG(WS_LOG_DEBUG, "No forward callback set for " + "forwarding channel, failing channel open"); + } fail_reason = OPEN_ADMINISTRATIVELY_PROHIBITED; ret = WS_ERROR; } @@ -13417,6 +13744,19 @@ 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. Only applies when the + * advertised list is derived from ctx->publicKeyAlgo: an application + * that set its own algoListKey supplies the list directly and never + * reads these slots. */ + 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; @@ -13993,7 +14333,7 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, if (pubKeyDer != NULL) WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); - if (ret != 0) { + if (ret != 0 && ret != WS_MEMORY_E) { WLOG(WS_LOG_DEBUG, "SendKexDhReply: cert store RSA pubkey " "decode failed %d", ret); @@ -14134,17 +14474,10 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, ret = wc_ecc_init_ex(&sigKeyBlock_ptr->sk.ecc.key, heap, INVALID_DEVID); scratch = 0; - #ifdef WOLFSSH_TPM - if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { - /* No private key in RAM; take the public point from the TPM. */ - ret = wolfTPM2_EccKey_TpmToWolf(ssh->ctx->tpmDev, - ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.ecc.key); - if (ret != 0) - ret = WS_ECC_E; - } - else - #endif /* WOLFSSH_TPM */ #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* A slot is never both cert-store and TPM backed; testing the + * cert store first matches the RSA case above and both SignH* + * helpers. */ 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 @@ -14168,7 +14501,7 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, if (pubKeyDer != NULL) WFREE(pubKeyDer, heap, DYNTYPE_PUBKEY); - if (ret != 0) { + if (ret != 0 && ret != WS_MEMORY_E) { WLOG(WS_LOG_DEBUG, "SendKexDhReply: cert store ECC pubkey " "decode failed %d", ret); @@ -14183,6 +14516,16 @@ static int SendKexGetSigningKey(WOLFSSH* ssh, } else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ + #ifdef WOLFSSH_TPM + if (ret == 0 && ssh->ctx->privateKey[keyIdx].isTpm) { + /* No private key in RAM; take the public point from the TPM. */ + ret = wolfTPM2_EccKey_TpmToWolf(ssh->ctx->tpmDev, + ssh->ctx->tpmKey, &sigKeyBlock_ptr->sk.ecc.key); + if (ret != 0) + ret = WS_ECC_E; + } + else + #endif /* WOLFSSH_TPM */ if (ret == 0) ret = wc_EccPrivateKeyDecode(ssh->ctx->privateKey[keyIdx].key, &scratch, &sigKeyBlock_ptr->sk.ecc.key, @@ -15384,51 +15727,74 @@ static int KeyAgreeEcdhMlKem_server(WOLFSSH* ssh, byte hashId, #ifdef WOLFSSH_WINDOWS_CERT_STORE +/* Every caller of these helpers sits in an RSA- or ECDSA-only region, so + * guard them the same way to avoid unused-static warnings on a build with + * both disabled (a degenerate combination the store loader rejects). */ +#if !defined(WOLFSSH_NO_RSA) || !defined(WOLFSSH_NO_ECDSA) /* 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; 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); + /* Heap-allocate unconditionally; DecodedCert is several KB and this is + * called on an already deep KEX path. */ + dCert = (struct DecodedCert*)WMALLOC(sizeof(struct DecodedCert), + heap, DYNTYPE_CERT); + if (dCert == NULL) { + return WS_MEMORY_E; + } + + wc_InitDecodedCert(dCert, certDer, certDerSz, heap); + ret = wc_ParseCert(dCert, CERT_TYPE, NO_VERIFY, NULL); if (ret == 0) { - ret = wc_GetPubKeyDerFromCert(&dCert, NULL, &pubKeyDerSz); - if (ret == LENGTH_ONLY_E) { + ret = wc_GetPubKeyDerFromCert(dCert, NULL, &pubKeyDerSz); + if (ret == WC_NO_ERR_TRACE(LENGTH_ONLY_E) && pubKeyDerSz > 0) { ret = 0; pubKeyDer = (byte*)WMALLOC(pubKeyDerSz, heap, DYNTYPE_PUBKEY); if (pubKeyDer == NULL) ret = WS_MEMORY_E; } + else { + /* The sizing call must report LENGTH_ONLY_E and a non-zero + * size; anything else is an error, so the copy call below can + * never run with a NULL destination. */ + if (ret >= 0) + ret = WS_CRYPTO_FAILED; + } } if (ret == 0) - ret = wc_GetPubKeyDerFromCert(&dCert, pubKeyDer, &pubKeyDerSz); - wc_FreeDecodedCert(&dCert); + ret = wc_GetPubKeyDerFromCert(dCert, pubKeyDer, &pubKeyDerSz); + wc_FreeDecodedCert(dCert); + WFREE(dCert, heap, DYNTYPE_CERT); 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; } -#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 @@ -15459,22 +15825,50 @@ 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) +#ifdef WOLFSSH_CERTS +/* 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 || cert == NULL || certSz == 0) { + return NULL; + } + + /* Prefer the slot registered under the exact algorithm id: the store + * loader registers the same certificate under both the plain and x509v3 + * ids, so a base-id match alone cannot tell those two slots apart. */ + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + pvtKey = &ctx->privateKey[i]; + if (IsCertStoreKey(pvtKey) && pvtKey->publicKeyFmt == keyId && + pvtKey->cert != NULL && pvtKey->certSz == certSz && + WMEMCMP(pvtKey->cert, cert, certSz) == 0) { + return pvtKey; + } + } + + /* Base-id fallback pass. Currently unreachable: every caller passes a + * keyId from NameToId() of the declared x509v3 name, and the loader + * registers the certificate under that exact id too, so any slot this + * pass could match already matched above. Kept as future-proofing for + * signature-variant ids (e.g. ID_RSA_SHA2_256 resolving to the + * ID_SSH_RSA slot) if a caller ever passes one. */ 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; } } @@ -15485,11 +15879,30 @@ static const WOLFSSH_PVT_KEY* FindCertStoreKey(const WOLFSSH_CTX* ctx, #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) +{ + 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 0; +} + + /* 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; @@ -15501,12 +15914,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; @@ -15526,19 +15940,74 @@ static int CertStoreEccSigToRs(const byte* sig, word32 sigSz, return ret; } + + +/* Self-verify a cert-store ECDSA signature (raw r and s) against the + * certificate's public key, shared by the KEX (SignHEcdsa) and user-auth + * (BuildUserAuthRequestEccCert) signing paths. Returns WS_SUCCESS when the + * signature verifies, WS_ECC_E when it does not, WS_MEMORY_E on allocation + * failure. */ +static int CertStoreEccSelfVerify(const byte* r, word32 rSz, + const byte* s, word32 sSz, const byte* digest, word32 digestSz, + ecc_key* key, void* heap) +{ + byte* derSig; + word32 derSigSz; + int verified; + int ret; +#ifndef WOLFSSH_SMALL_STACK + byte derSig_s[ECC_MAX_SIG_SIZE]; +#endif + + ret = WS_SUCCESS; + verified = 0; + WOLFSSH_UNUSED(heap); +#ifdef WOLFSSH_SMALL_STACK + derSig = (byte*)WMALLOC(ECC_MAX_SIG_SIZE, heap, DYNTYPE_TEMP); + if (derSig == NULL) { + ret = WS_MEMORY_E; + } +#else + derSig = derSig_s; +#endif + + if (ret == WS_SUCCESS) { + derSigSz = ECC_MAX_SIG_SIZE; + ret = wc_ecc_rs_raw_to_sig(r, rSz, s, sSz, derSig, &derSigSz); + if (ret == 0) { + ret = wc_ecc_verify_hash(derSig, derSigSz, digest, digestSz, + &verified, key); + } + if (ret != 0 || verified != 1) { + WLOG(WS_LOG_DEBUG, "CertStoreEccSelfVerify: Cert store " + "signature failed self-verify"); + ret = WS_ECC_E; + } + else { + ret = WS_SUCCESS; + } + } +#ifdef WOLFSSH_SMALL_STACK + if (derSig != NULL) + WFREE(derSig, heap, DYNTYPE_TEMP); +#endif + + 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. + * For RSA, expects encoded signature (digest + OID) in the data parameter. + * For ECDSA, expects raw hash in the data parameter. + * sigSz is in/out: on input the capacity of sig, which NCryptSignHash uses + * as the output buffer size; on output the produced signature length. */ 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; @@ -15551,16 +16020,16 @@ static int SignWithCertStoreKey(WOLFSSH* ssh, 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) { + 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; @@ -15570,8 +16039,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; } @@ -15618,7 +16087,15 @@ 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 if (dwSigLen == 0 || dwSigLen > cbSignature) { + /* Do not trust a KSP-reported length past the caller's + * capacity; downstream copies *sigSz bytes from sig. */ + WLOG(WS_LOG_DEBUG, "SignWithCertStoreKey: Bad signature " + "length %lu (capacity %lu)", (unsigned long)dwSigLen, + (unsigned long)cbSignature); ret = WS_CRYPTO_FAILED; } else { *sigSz = dwSigLen; @@ -15627,14 +16104,21 @@ 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); return ret; } +#endif /* !WOLFSSH_NO_RSA || !WOLFSSH_NO_ECDSA */ #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -15689,8 +16173,31 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } if (ret == WS_SUCCESS) { + #ifdef WOLFSSH_TPM + byte signedByTpm = 0; + #endif + 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; *sigSz carries the sig + * buffer capacity in and the produced length out. */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, encSig, encSigSz, + 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) { @@ -15698,32 +16205,24 @@ 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 { WLOG(WS_LOG_DEBUG, "SignHRsa: Bad TPM Sign"); ret = WS_RSA_E; } + signedByTpm = 1; } 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) { - WLOG(WS_LOG_DEBUG, "SignHRsa: Cert store sign failed"); - } - } - else - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { - /* Use traditional key signing */ + /* Use traditional key signing; *sigSz holds the capacity the + * caller gave the sig buffer, same as the cert-store branch. */ ret = wc_RsaSSL_Sign(encSig, encSigSz, sig, - KEX_SIG_SIZE, &sigKey->sk.rsa.key, + *sigSz, &sigKey->sk.rsa.key, ssh->rng); if (ret <= 0) { WLOG(WS_LOG_DEBUG, "SignHRsa: Bad RSA Sign"); @@ -15734,27 +16233,18 @@ static int SignHRsa(WOLFSSH* ssh, byte* sig, word32* sigSz, ret = WS_SUCCESS; } } - } - if (ret == WS_SUCCESS - #ifdef WOLFSSH_TPM - && !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 */ - { + /* Self-verify keyed to the branch that actually signed, not to the + * handshake flag, so a future divergence between the two cannot skip + * the check. */ + if (ret == WS_SUCCESS + #ifdef WOLFSSH_TPM + && !signedByTpm + #endif + ) { + /* 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"); } @@ -15828,6 +16318,20 @@ static int SignHEcdsa(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 SignHRsa() and SendKexGetSigningKey(). */ + if (IsCertStoreKey(sigKey->pvtKey)) { + /* Use cert store signing abstraction - ECDSA uses raw hash. + * The signature is self-verified after the r/s split below. */ + ret = SignWithCertStoreKey(ssh, sigKey->pvtKey, digest, digestSz, + sig, sigSz); + if (ret != WS_SUCCESS) { + WLOG(WS_LOG_DEBUG, "SignHEcdsa: Cert store sign failed"); + } + } + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM if (useTpm) { ret = wolfTPM2_SignHashScheme(ssh->ctx->tpmDev, ssh->ctx->tpmKey, @@ -15843,23 +16347,6 @@ static int SignHEcdsa(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 - 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 - #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { /* Use traditional key signing */ ret = wc_ecc_sign_hash(digest, digestSz, sig, sigSz, ssh->rng, @@ -15889,6 +16376,24 @@ static int SignHEcdsa(WOLFSSH* ssh, byte* sig, word32* sigSz, } if (ret == WS_SUCCESS) { + #ifdef WOLFSSH_WINDOWS_CERT_STORE + /* NCryptSignHash for ECDSA returns raw r||s (each half the curve + * size), NOT DER-encoded. Split directly. Same branch order as the + * signing dispatch above. */ + if (IsCertStoreKey(sigKey->pvtKey)) { + ret = CertStoreEccSigToRs(sig, *sigSz, + CertStoreCurveSzForId(sigKey->pvtKey->publicKeyFmt), + r, &rSz, s, &sSz); + if (ret == WS_SUCCESS) { + /* Self-verify with the certificate public key decoded into + * sk.ecc.key by SendKexGetSigningKey(), matching the RSA + * path's wolfSSH_RsaVerify() check. */ + ret = CertStoreEccSelfVerify(r, rSz, s, sSz, digest, digestSz, + &sigKey->sk.ecc.key, ssh->ctx->heap); + } + } + else + #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #ifdef WOLFSSH_TPM if (useTpm) { /* TPM returns raw R||S, each half left-padded to the curve size. */ @@ -15911,13 +16416,6 @@ 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. */ - 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) { @@ -16015,6 +16513,12 @@ static int SignHMlDsa(WOLFSSH* ssh, byte* sig, word32* sigSz, #endif +/* Sign the session hash with the negotiated host key. sigSz is in/out: on + * input the capacity of the sig buffer, which the signing backends (e.g. + * wc_RsaSSL_Sign, NCryptSignHash) trust as the output buffer size; on + * output the produced signature length. Callers must set *sigSz before + * every call -- a prior call rewrites it to the produced length, which is + * smaller than the capacity. */ static int SignH(WOLFSSH* ssh, byte* sig, word32* sigSz, struct wolfSSH_sigKeyBlockFull *sigKey) { @@ -18197,6 +18701,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, @@ -18205,34 +18717,44 @@ 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; + byte* pubKeyDer = NULL; + word32 pubKeyDerSz = 0; - 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); + 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); + /* Keep wolfCrypt codes out of the wolfSSH error space, + * matching the KEX cert-store call sites. */ + if (ret != 0 && ret != WS_MEMORY_E) { + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestRsaCert: " + "Bad public key decode from cert"); + ret = WS_CRYPTO_FAILED; } - if (pubKeyDer != NULL) - WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); } - } else + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + 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) { @@ -18356,45 +18878,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, + 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, @@ -18735,31 +19258,36 @@ 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); + 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); + /* Keep wolfCrypt codes out of the wolfSSH error space, + * matching the KEX cert-store call sites. */ + if (ret != 0 && ret != WS_MEMORY_E) { + WLOG(WS_LOG_DEBUG, "PrepareUserAuthRequestEccCert: " + "Bad public key decode from cert"); + ret = WS_CRYPTO_FAILED; } - if (pubKeyDer != NULL) - WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); } - } else + if (pubKeyDer != NULL) + WFREE(pubKeyDer, ssh->ctx->heap, DYNTYPE_PUBKEY); + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { #if 0 @@ -18783,10 +19311,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); + } } } @@ -18881,6 +19421,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); @@ -18890,39 +19437,46 @@ 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); + digest, digestSz, 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"); } - } 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 + if (ret == WS_SUCCESS) { + /* Self-verify against the certificate public key decoded by + * PrepareUserAuthRequestEccCert(), matching every other + * cert-store signing path. */ + ret = CertStoreEccSelfVerify(r, rSz, s, sSz, digest, digestSz, + &keySig->ks.ecc.key, ssh->ctx->heap); + } + } + else #endif /* WOLFSSH_WINDOWS_CERT_STORE */ { if (ret == WS_SUCCESS) { @@ -23827,6 +24381,25 @@ int wolfSSH_TestParseECCPubKey(WOLFSSH* ssh, byte* pubKey, word32 pubKeySz) return ret; } +#ifdef WOLFSSH_CERTS +/* Test hook for the certificate host key parser and its curve binding. The + * caller sets ssh->handshake->pubKeyId to the negotiated algorithm; pubKey + * holds an RFC 6187 chain blob whose chain must verify against the CTX's + * cert manager. */ +int wolfSSH_TestParseECCPubKeyCert(WOLFSSH* ssh, byte* pubKey, word32 pubKeySz) +{ + struct wolfSSH_sigKeyBlock sigKeyBlock; + int ret; + + WMEMSET(&sigKeyBlock, 0, sizeof(sigKeyBlock)); + sigKeyBlock.useEcc = 1; + ret = ParseECCPubKeyCert(ssh, &sigKeyBlock, pubKey, pubKeySz); + FreePubKey(&sigKeyBlock); + + return ret; +} +#endif /* WOLFSSH_CERTS */ + #endif /* !WOLFSSH_NO_ECDSA */ #ifndef WOLFSSH_NO_ED25519 diff --git a/src/ssh.c b/src/ssh.c index 3872df931..c96ab5e65 100644 --- a/src/ssh.c +++ b/src/ssh.c @@ -24,6 +24,28 @@ */ +/* CompareStringOrdinal() and friends need a Vista-or-later SDK profile; + * mingw-w64 has historically defaulted _WIN32_WINNT to pre-Vista, so raise + * the floor before the first header that pulls in (wolfssh/ssh.h + * via port.h does). The undefined case is raised only under mingw: MSVC's + * SDK defaults an undefined _WIN32_WINNT to its newest profile, which a + * 0x0600 define here would silently lower. An explicit pre-Vista target is + * raised on any compiler since this file cannot build against it. WINVER is + * pinned alongside so the two profiles cannot disagree. Keyed on _WIN32 + * rather than WOLFSSH_WINDOWS_CERT_STORE: this is the only macro guaranteed + * defined this early, since a user_settings.h build defines + * WOLFSSH_WINDOWS_CERT_STORE only once pulls in settings.h + * below, after has already been seen. Harmless for + * non-cert-store Windows builds. */ +#if defined(_WIN32) && \ + ((defined(__MINGW32__) && !defined(_WIN32_WINNT)) || \ + (defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0600)) + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0600 + #undef WINVER + #define WINVER 0x0600 +#endif + #ifdef HAVE_CONFIG_H #include #endif @@ -41,8 +63,24 @@ #include #include #include + /* Fallbacks for SDKs that predate these wincrypt.h/ncrypt.h + * definitions. The values must match the SDK headers exactly. The + * CERT_SYSTEM_STORE_* location constants are consumed only by + * src/certman.c, which carries its own fallbacks. */ #ifndef CERT_NCRYPT_KEY_SPEC - #define CERT_NCRYPT_KEY_SPEC 0x00000003 + #define CERT_NCRYPT_KEY_SPEC 0xFFFFFFFF + #endif + #ifndef CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG + #define CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG 0x00040000 + #endif + #ifndef NCRYPT_KEY_USAGE_PROPERTY + #define NCRYPT_KEY_USAGE_PROPERTY L"Key Usage" + #endif + #ifndef NCRYPT_ALLOW_DECRYPT_FLAG + #define NCRYPT_ALLOW_DECRYPT_FLAG 0x00000001 + #endif + #ifndef NCRYPT_ALLOW_SIGNING_FLAG + #define NCRYPT_ALLOW_SIGNING_FLAG 0x00000002 #endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -3311,30 +3349,141 @@ int wolfSSH_CTX_AddRootCert_file(WOLFSSH_CTX* ctx, const char* name) #endif /* !NO_FILESYSTEM && !WOLFSSH_USER_FILESYSTEM */ #ifdef WOLFSSH_WINDOWS_CERT_STORE -/* Find the certificate in hStore whose Common Name exactly matches - * subjectName. subjectName may include a leading "CN=" prefix. +/* Result of CertKeyCanSign(): whether the certificate's private key can + * actually be used for signing. NONE and NOSIGN are both unusable, but are + * kept distinct so the final diagnostic can tell an inaccessible key (fix + * the ACL) from a key enrolled without signing usage (enroll a signing + * certificate). */ +#define WS_CERT_KEY_NONE 0 /* key not acquirable (missing, ACL, CSP) */ +#define WS_CERT_KEY_UNKNOWN 1 /* key acquired but usage is not reported */ +#define WS_CERT_KEY_SIGNS 2 /* key acquired and reports signing usage */ +#define WS_CERT_KEY_NOSIGN 3 /* key acquired, usage excludes signing */ + +/* Classify the certificate's private key for signing use. + * 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. Several smart-card / third-party KSPs do not + * implement NCRYPT_KEY_USAGE_PROPERTY; those keys are reported as + * usage-unknown so the caller can rank them below a key that definitely + * signs instead of failing open on the first one enumerated. */ +static int CertKeyCanSign(PCCERT_CONTEXT pCertContext) +{ + HCRYPTPROV_OR_NCRYPT_KEY_HANDLE hKey = 0; + DWORD dwKeySpec = 0; + BOOL fCallerFree = FALSE; + DWORD keyUsage = 0; + DWORD cbOut = 0; + SECURITY_STATUS status; + int canSign = WS_CERT_KEY_SIGNS; + + if (!CryptAcquireCertificatePrivateKey(pCertContext, + CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG | CRYPT_ACQUIRE_SILENT_FLAG, + NULL, &hKey, &dwKeySpec, &fCallerFree)) { + return WS_CERT_KEY_NONE; + } + + /* CRYPT_ACQUIRE_ONLY_NCRYPT_KEY_FLAG means dwKeySpec is documented to + * always be CERT_NCRYPT_KEY_SPEC; verify before handing the union-typed + * handle to CNG. A legacy CSP key cannot sign through NCryptSignHash() + * later, so reject it here rather than misreport it as usable. */ + if (dwKeySpec != CERT_NCRYPT_KEY_SPEC) { + if (fCallerFree) { + CryptReleaseContext(hKey, 0); + } + return WS_CERT_KEY_NONE; + } + + /* An acquirable key is not necessarily a signing key: an enterprise My + * store commonly holds a keyEncipherment-only RSA certificate next to + * the signing one with the same CN. Require NCRYPT_ALLOW_SIGNING_FLAG + * when the provider exposes the usage property. */ + status = NCryptGetProperty(hKey, NCRYPT_KEY_USAGE_PROPERTY, + (PBYTE)&keyUsage, (DWORD)sizeof(keyUsage), &cbOut, 0); + if (status == 0 && cbOut == (DWORD)sizeof(keyUsage)) { + if ((keyUsage & NCRYPT_ALLOW_SIGNING_FLAG) == 0) { + WLOG(WS_LOG_DEBUG, "CertKeyCanSign: key usage 0x%lx does not " + "allow signing", (unsigned long)keyUsage); + canSign = WS_CERT_KEY_NOSIGN; + } + } + else { + WLOG(WS_LOG_DEBUG, "CertKeyCanSign: NCRYPT_KEY_USAGE_PROPERTY not " + "readable (status 0x%lx), key usage unknown", + (unsigned long)status); + canSign = WS_CERT_KEY_UNKNOWN; + } + + if (fCallerFree) { + /* Only NCRYPT keys reach this point; a CSP key was released and + * rejected right after acquisition. */ + NCryptFreeObject(hKey); + } + + return canSign; +} + + +/* 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". 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 ordinal and case insensitive, so it cannot + * change with the thread locale. Candidates are ranked by how usable they + * are: time-valid whose key definitely signs first, then time-valid whose + * provider does not report key usage, then (only when + * WOLFSSH_CERT_STORE_ALLOW_EXPIRED is defined) expired over not yet + * valid, then the latest NotAfter. Without that opt-in, a match that is + * not time-valid fails with WS_CERT_EXPIRED_E rather than silently + * presenting an expired credential. 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 match exists. The caller frees it with + * CertFreeCertificateContext. + * Returns WS_SUCCESS on success, WS_CRYPTO_FAILED when the CN matched + * only certificates whose private key is not accessible. */ +static int FindCertByExactCN(void* heap, HCERTSTORE hStore, + const wchar_t* subjectName, PCCERT_CONTEXT* out) { PCCERT_CONTEXT pCertContext; + PCCERT_CONTEXT keyedMatch; + PCCERT_CONTEXT validUnknown; const wchar_t* cn; wchar_t* certCn; DWORD certCnSz; int match; + int hasKey; + int timeValidity; + int keyedEarly; + int keyedSigns; + int keylessSeen; + int nosignSeen; + int better; + int ret; + + *out = NULL; + ret = WS_SUCCESS; + keyedEarly = 0; + keyedSigns = 0; + keylessSeen = 0; + nosignSeen = 0; + validUnknown = NULL; /* 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 && + CompareStringOrdinal(cn, 3, L"CN=", 3, TRUE) == CSTR_EQUAL) { cn = cn + 3; } + if (*cn == L'\0') { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Empty common name requested"); + return WS_BAD_ARGUMENT; + } pCertContext = NULL; + keyedMatch = NULL; for (;;) { /* Passing the previous context frees it and continues the search. */ pCertContext = CertFindCertificateInStore(hStore, @@ -3348,123 +3497,246 @@ 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) { + match = (certCnSz > 1 && + CompareStringOrdinal(certCn, -1, cn, -1, TRUE) == CSTR_EQUAL); + WFREE(certCn, heap, DYNTYPE_TEMP); + if (!match) { + continue; + } + + /* 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 == WS_CERT_KEY_SIGNS && timeValidity == 0) { + break; + } + + if (hasKey == WS_CERT_KEY_UNKNOWN && timeValidity == 0) { + /* Time-valid but the provider does not report key usage: keep + * the first one as a candidate and keep searching for a + * sibling that definitely signs, so an encryption-only key in + * a usage-silent KSP cannot shadow the signing one. */ + if (validUnknown == NULL) { + validUnknown = CertDuplicateCertificateContext(pCertContext); + if (validUnknown == NULL) { + ret = WS_MEMORY_E; + } + } + } + else if (hasKey == WS_CERT_KEY_SIGNS || + hasKey == WS_CERT_KEY_UNKNOWN) { + /* Deterministic fallback ranking, matching the time-valid path: + * a key that definitely signs beats one whose usage is unknown, + * then expired beats not yet valid, and within the same class + * the latest NotAfter wins. */ + better = 0; + if (keyedMatch == NULL) { + better = 1; + } + else if (keyedSigns != (hasKey == WS_CERT_KEY_SIGNS)) { + better = (hasKey == WS_CERT_KEY_SIGNS); + } + else if (keyedEarly && timeValidity > 0) { + better = 1; + } + else if (keyedEarly == (timeValidity < 0) && + CompareFileTime(&pCertContext->pCertInfo->NotAfter, + &keyedMatch->pCertInfo->NotAfter) > 0) { + better = 1; + } + if (better) { + if (keyedMatch != NULL) { + CertFreeCertificateContext(keyedMatch); + } + keyedMatch = CertDuplicateCertificateContext(pCertContext); + keyedEarly = (timeValidity < 0); + keyedSigns = (hasKey == WS_CERT_KEY_SIGNS); + if (keyedMatch == NULL) { + ret = WS_MEMORY_E; + } + } + } + else if (hasKey == WS_CERT_KEY_NOSIGN) { + nosignSeen = 1; + } + else { + keylessSeen = 1; + } + + if (ret != WS_SUCCESS) { + CertFreeCertificateContext(pCertContext); + pCertContext = NULL; break; } } - return pCertContext; + /* 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 (validUnknown != NULL) { + WLOG(WS_LOG_INFO, "FindCertByExactCN: No candidate reports " + "signing usage; using a time-valid '%ls' whose key " + "usage is unknown", subjectName); + pCertContext = validUnknown; + validUnknown = NULL; + } + else if (keyedMatch != NULL) { +#ifdef WOLFSSH_CERT_STORE_ALLOW_EXPIRED + /* WS_LOG_ERROR so the fallback is as visible as logging in + * this build allows; still only compiled in when logging is. */ + WLOG(WS_LOG_ERROR, "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 + /* Fail closed by default: in a non-logging build a silent + * fallback would present an expired host credential with + * WS_SUCCESS and no local indication of the cause. Define + * WOLFSSH_CERT_STORE_ALLOW_EXPIRED to opt into the old + * behavior. */ + WLOG(WS_LOG_ERROR, "FindCertByExactCN: '%ls' matched only a %s " + "certificate; rejecting (define " + "WOLFSSH_CERT_STORE_ALLOW_EXPIRED to use it anyway)", + subjectName, keyedEarly ? "not yet valid" : "expired"); + ret = WS_CERT_EXPIRED_E; +#endif + } + else if (keylessSeen) { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: '%ls' matched only " + "certificates with no usable private key", subjectName); + ret = WS_CRYPTO_FAILED; + } + else if (nosignSeen) { + WLOG(WS_LOG_ERROR, "FindCertByExactCN: '%ls' matched only " + "certificates whose private key is not a signing key; " + "enroll a signing certificate", subjectName); + ret = WS_CRYPTO_FAILED; + } + } + if (keyedMatch != NULL) { + CertFreeCertificateContext(keyedMatch); + } + if (validUnknown != NULL) { + CertFreeCertificateContext(validUnknown); + } + + if (ret != WS_SUCCESS) { + if (pCertContext != NULL) { + CertFreeCertificateContext(pCertContext); + } + WLOG(WS_LOG_ERROR, "FindCertByExactCN: Failed, ret = %d", ret); + } + else { + *out = 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; + 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 */ - 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; - } +/* 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 (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; + if (slot->cert != NULL) { + WFREE(slot->cert, heap, DYNTYPE_CERT); } - 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; + 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 + * 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, CertStoreSlot* slot) +{ + /* Validating the index here, before anything is mutated, is what lets + * CommitCertStoreSlot() be infallible. */ + if (keyIdx >= WOLFSSH_MAX_PVT_KEYS) { + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Slot index out of range"); + return WS_BAD_ARGUMENT; } - /* 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++; + /* 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; + + slot->cert = (byte*)WMALLOC(slot->certSz, heap, DYNTYPE_CERT); + slot->context = CertDuplicateCertificateContext(pCertContext); + if (slot->cert == NULL || slot->context == NULL) { + FreeCertStoreSlot(heap, slot); + WLOG(WS_LOG_ERROR, "PrepCertStoreSlot: Memory allocation failed"); + return WS_MEMORY_E; } - pvtKey = &ctx->privateKey[keyIdx]; + WMEMCPY(slot->cert, pCertContext->pbCertEncoded, slot->certSz); + + return WS_SUCCESS; +} + + +/* 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. Infallible by design: PrepCertStoreSlot() + * validates the slot index before anything is prepared, which is what + * makes the two-slot commit in wolfSSH_CTX_UsePrivateKey_fromStore() + * all-or-nothing. */ +static void CommitCertStoreSlot(WOLFSSH_CTX* ctx, CertStoreSlot* slot) +{ + WOLFSSH_PVT_KEY* pvtKey; + void* heap; + + heap = ctx->heap; + pvtKey = &ctx->privateKey[slot->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) { + /* Defensive only: wolfSSH_CTX_UsePrivateKey_fromStore() rejects a + * slot holding file-based credentials before preparing it, the + * same way SetHostPrivateKey() rejects the mirror-image order. */ + 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; @@ -3472,34 +3744,61 @@ 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->dwFlags = dwFlags; - pvtKey->cert = certBuf; - pvtKey->certSz = certSz; + pvtKey->certStoreContext = (void*)slot->context; + pvtKey->cert = slot->cert; + pvtKey->certSz = slot->certSz; - return WS_SUCCESS; + /* Ownership moved to the context. */ + WMEMSET(slot, 0, sizeof(*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 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 compiles out the + * x509v3 algorithm for the key type (WOLFSSH_NO_SSH_RSA_SHA1, per-curve + * ECDSA gates) and for RSA when SHA-1 is soft disabled, since + * x509v3-ssh-rsa signs with SHA-1 and is then never advertised. * returns WS_SUCCESS on success */ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, @@ -3510,7 +3809,18 @@ 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; + byte haveCertSlot; +#ifndef WOLFSSH_NO_ECDSA + const byte* params = NULL; + DWORD paramsSz = 0; +#endif WLOG(WS_LOG_DEBUG, "Entering wolfSSH_CTX_UsePrivateKey_fromStore()"); @@ -3519,18 +3829,42 @@ int wolfSSH_CTX_UsePrivateKey_fromStore(WOLFSSH_CTX* ctx, return WS_BAD_ARGUMENT; } - /* Open the certificate store */ + /* 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; + } + + /* 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); - return WS_FATAL_ERROR; + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Failed to " + "open store, error: %lu", (unsigned long)GetLastError()); + return WS_BAD_FILE_E; } - /* 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_CRYPTO_FAILED) { + CertCloseStore(hStore, 0); + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate " + "matched but its private key is not usable for signing; see " + "the FindCertByExactCN message above for whether the key is " + "inaccessible (check key permissions for the service account) " + "or enrolled without signing usage"); + return WS_CRYPTO_FAILED; + } + if (ret != WS_SUCCESS) { + CertCloseStore(hStore, 0); + return ret; + } if (pCertContext == NULL) { CertCloseStore(hStore, 0); WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: Certificate " @@ -3542,121 +3876,200 @@ 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) { + if (strcmp(pPubKeyInfo->Algorithm.pszObjId, szOID_RSA_RSA) == 0) { + /* 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: " + "No usable RSA signature algorithm is 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); + 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. */ + params = pPubKeyInfo->Algorithm.Parameters.pbData; + 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(certStoreOidP256) && + WMEMCMP(params, certStoreOidP256, + sizeof(certStoreOidP256)) == 0) { keyId = ID_ECDSA_SHA2_NISTP256; } + else + #endif + #ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + 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(certStoreOidP521) && + WMEMCMP(params, certStoreOidP521, + sizeof(certStoreOidP521)) == 0) { + keyId = ID_ECDSA_SHA2_NISTP521; + } + else + #endif + { + /* With all curves disabled paramsSz is set but never read. */ + WOLFSSH_UNUSED(paramsSz); + 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 { - 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; } - /* 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); + /* FindCertByExactCN only returns a certificate whose private key + * CertKeyCanSign() could acquire, so key access is already verified. */ + + /* 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. 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 = FindPvtKeyIdx(ctx, keyId); + /* Mirror of SetHostPrivateKey()/SetHostCertificate(): a store key must + * not silently replace file- or TPM-based credentials already loaded + * for this algorithm, so the mixed configuration is rejected in both + * load orders. Replacing a previous store key is still allowed. */ + if (keyIdx != WOLFSSH_MAX_PVT_KEYS && + !ctx->privateKey[keyIdx].useCertStore) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: A host key " + "for this algorithm is already loaded from a file or TPM; it " + "cannot be paired with a store key"); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_BAD_ARGUMENT; + } + 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. haveCertSlot rather + * than a certIdx sentinel, so the "skip" marker cannot be confused with + * an index a full table legitimately computes. */ + certId = CertTypeForId(keyId); +#if !defined(WOLFSSH_NO_SHA1_SOFT_DISABLE) + /* x509v3-ssh-rsa signs with SHA-1; with SHA-1 soft disabled do not + * register a slot for it. Note the canned lists only gate the client + * default; a server would advertise the slot via RefreshPublicKeyAlgo, + * which is exactly why it must not be registered. If an earlier + * file-based load already claimed that slot, reject the mixed + * configuration outright rather than leaving a stale file key and + * certificate advertised beside the store key. */ + if (certId == ID_X509V3_SSH_RSA) { + if (FindPvtKeyIdx(ctx, certId) != WOLFSSH_MAX_PVT_KEYS) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: An " + "x509v3-ssh-rsa file host key/certificate is already " + "loaded; it cannot be paired with a store key"); CertFreeCertificateContext(pCertContext); CertCloseStore(hStore, 0); - return WS_CRYPTO_FAILED; + return WS_BAD_ARGUMENT; } - /* 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); - } + certId = keyId; + } +#endif + certIdx = 0; + haveCertSlot = 0; + if (certId != keyId) { + certIdx = FindPvtKeyIdx(ctx, certId); + /* Same mixed-configuration rejection for the x509v3 slot, so a + * file HostCertificate is never silently destroyed either. */ + if (certIdx != WOLFSSH_MAX_PVT_KEYS && + !ctx->privateKey[certIdx].useCertStore) { + WLOG(WS_LOG_ERROR, "wolfSSH_CTX_UsePrivateKey_fromStore: A host " + "certificate for this algorithm is already loaded from a " + "file; it cannot be paired with a store key"); + CertFreeCertificateContext(pCertContext); + CertCloseStore(hStore, 0); + return WS_BAD_ARGUMENT; + } + if (certIdx == WOLFSSH_MAX_PVT_KEYS) { + certIdx = newCount++; } - WLOG(WS_LOG_DEBUG, "wolfSSH_CTX_UsePrivateKey_fromStore: Private key " - "access verified successfully"); + haveCertSlot = 1; + } + 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); } - /* 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 (newCount > WOLFSSH_MAX_PVT_KEYS) { + 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) { - 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, + &keySlot); + } + if (ret == WS_SUCCESS && haveCertSlot) { + ret = PrepCertStoreSlot(ctx->heap, certId, certIdx, pCertContext, + &certSlot); + } + + if (ret == WS_SUCCESS) { + /* Committing cannot fail (see CommitCertStoreSlot), so once both + * slots are prepared the context update is atomic from the caller's + * point of view. */ + CommitCertStoreSlot(ctx, &keySlot); + if (haveCertSlot) { + CommitCertStoreSlot(ctx, &certSlot); } + ctx->privateKeyCount = newCount; + } + else { + FreeCertStoreSlot(ctx->heap, &keySlot); + FreeCertStoreSlot(ctx->heap, &certSlot); } /* Each registered slot holds its own reference on the certificate @@ -3674,9 +4087,67 @@ 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; } + + +/* Report the certificate a cert-store host key is bound to, so an + * application can offer it for certificate user auth without reaching into + * the private CTX layout. Returns the first cert-store backed slot + * registered under an x509v3 algorithm: cert/certSz point at the DER copy + * owned by the CTX (valid until the CTX is freed or the slot replaced) and + * algoName at the static SSH algorithm name string. Any out pointer may be + * NULL to skip it. Returns WS_SUCCESS, WS_BAD_ARGUMENT on a NULL ctx, or + * WS_FATAL_ERROR when no such slot exists (e.g. the x509v3 form of the key + * type is compiled out or soft disabled). */ +int wolfSSH_CTX_GetCertStoreCert(WOLFSSH_CTX* ctx, const byte** cert, + word32* certSz, const char** algoName) +{ + const WOLFSSH_PVT_KEY* pvtKey; + word32 i; + int isX509Id; + + if (ctx == NULL) { + return WS_BAD_ARGUMENT; + } + + for (i = 0; i < ctx->privateKeyCount && i < WOLFSSH_MAX_PVT_KEYS; i++) { + pvtKey = &ctx->privateKey[i]; + if (!pvtKey->useCertStore || pvtKey->certStoreContext == NULL || + pvtKey->key != NULL || + pvtKey->cert == NULL || pvtKey->certSz == 0) { + continue; + } + switch (pvtKey->publicKeyFmt) { + case ID_X509V3_SSH_RSA: + case ID_X509V3_ECDSA_SHA2_NISTP256: + case ID_X509V3_ECDSA_SHA2_NISTP384: + case ID_X509V3_ECDSA_SHA2_NISTP521: + isX509Id = 1; + break; + default: + isX509Id = 0; + break; + } + if (!isX509Id) { + continue; + } + if (cert != NULL) { + *cert = pvtKey->cert; + } + if (certSz != NULL) { + *certSz = pvtKey->certSz; + } + if (algoName != NULL) { + *algoName = IdToName(pvtKey->publicKeyFmt); + } + return WS_SUCCESS; + } + + return WS_FATAL_ERROR; +} #endif /* WOLFSSH_WINDOWS_CERT_STORE */ #endif /* WOLFSSH_CERTS */ diff --git a/tests/api.c b/tests/api.c index e432fdc2a..ba29b34bb 100644 --- a/tests/api.c +++ b/tests/api.c @@ -743,6 +743,10 @@ static void test_wolfSSH_CTX_UseCert_buffer(void) wolfSSH_CTX_UseCert_buffer(ctx, cert, certSz, WOLFSSH_FORMAT_PEM)); AssertIntEQ(1, ctx->privateKeyCount); AssertNotNull(ctx->privateKey[0].cert); + /* A certificate with no key behind it has no signing source, so + * RefreshPublicKeyAlgo must not advertise it yet; loading the matching + * key below is what makes the slot advertisable. */ + AssertIntEQ(0, ctx->publicKeyAlgoCount); #endif AssertIntEQ(WS_BAD_FILETYPE_E, diff --git a/tests/sftp.c b/tests/sftp.c index 2d8d940b7..7f5567550 100644 --- a/tests/sftp.c +++ b/tests/sftp.c @@ -136,6 +136,36 @@ static int checkLsSize(void) sizeof(inBuf)) == NULL) ? 1 : 0; } +/* a creat parse failure prints an error and creates nothing, so verify the + * tab-separated form actually created the file */ +static int checkLsHasCreatMtab(void) +{ + return (WSTRNSTR(inBuf, "test-creat-mtab", + sizeof(inBuf)) == NULL) ? 1 : 0; +} + +/* same for the leading-whitespace form */ +static int checkLsHasCreatWs(void) +{ + return (WSTRNSTR(inBuf, "test-creat-ws", + sizeof(inBuf)) == NULL) ? 1 : 0; +} + +/* same for the tab-separator form */ +static int checkLsHasCreatTab(void) +{ + return (WSTRNSTR(inBuf, "test-creat-tab", + sizeof(inBuf)) == NULL) ? 1 : 0; +} + +/* guards the anchored creat matcher: if "rm mycreat" were mis-dispatched to + * creat, its argument parse would fail and the rm would silently never run, + * leaving mycreat behind for this check to find */ +static int checkLsHasNoMycreat(void) +{ + return (WSTRNSTR(inBuf, "mycreat", sizeof(inBuf)) != NULL) ? 1 : 0; +} + static int checkCdNonexistent(void) { if (WSTRNSTR(inBuf, "Error changing directory", @@ -274,6 +304,10 @@ static const SftpTestCmd cmds[] = { { "rm test-get", NULL }, { "rm test-get-2", NULL }, { "rm test-creat-special", NULL }, + { "rm test-creat-ws", NULL }, + { "rm test-creat-tab", NULL }, + { "rm test-creat-mtab", NULL }, + { "rm mycreat", NULL }, /* --- test sequence starts here --- */ { "mkdir a", NULL }, @@ -310,6 +344,27 @@ static const SftpTestCmd cmds[] = { #endif { "chmod 600 test-get-2", NULL }, { "rm test-get-2", NULL }, + /* the creat matcher is anchored to the line start: an argument + * containing the substring must stay with its own command. A + * mis-dispatched "rm mycreat" would fail creat's argument parse and + * silently skip the rm, so the ls check would still find mycreat. */ + { "creat 0644 mycreat", NULL }, + { "rm mycreat", NULL }, + { "ls", checkLsHasNoMycreat }, + /* leading whitespace and a tab separator both reach the creat handler; + * a parse failure would skip creation silently (rm ignores a missing + * file), so check each with ls before removing */ + { " creat 0644 test-creat-ws", NULL }, + { "ls", checkLsHasCreatWs }, + { "rm test-creat-ws", NULL }, + { "creat\t0644 test-creat-tab", NULL }, + { "ls", checkLsHasCreatTab }, + { "rm test-creat-tab", NULL }, + /* tab between mode and path must also parse; a parse failure would skip + * creation silently (rm ignores a missing file), so check with ls */ + { "creat 0644\ttest-creat-mtab", NULL }, + { "ls", checkLsHasCreatMtab }, + { "rm test-creat-mtab", NULL }, { "ls -s", checkLsSize }, { "cd /nonexistent_path_xyz", checkCdNonexistent }, #if !defined(NO_WOLFSSH_DIR) && !defined(WOLFSSH_FATFS) diff --git a/tests/unit.c b/tests/unit.c index a0afec029..06c42fb69 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -47,21 +47,36 @@ #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) #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 + +/* 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 @@ -79,6 +94,24 @@ #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 + #ifndef CERT_SYSTEM_STORE_CURRENT_SERVICE + #define CERT_SYSTEM_STORE_CURRENT_SERVICE 0x00040000 + #endif + #ifndef CERT_SYSTEM_STORE_SERVICES + #define CERT_SYSTEM_STORE_SERVICES 0x00050000 + #endif + #ifndef CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY + #define CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY 0x00070000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY 0x00080000 + #endif + #ifndef CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE + #define CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE 0x00090000 + #endif #endif #ifdef WOLFSSH_SFTP @@ -12339,7 +12372,20 @@ 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. */ + +/* Guard by the actual users -- the promote tests, the root-CA half of + * test_SetCertManager(), and test_ParseECCPubKeyCert() -- to avoid + * -Wunused-function. */ +#if defined(WOLFSSH_TEST_CERTMAN_PROMOTE) || \ + (defined(WOLFSSH_TEST_SET_CERTMAN) && \ + defined(WOLFSSH_TEST_CERTMAN_ROOTCA)) || \ + (defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) && \ + !defined(NO_FILESYSTEM) && !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256)) /* Read a whole file into a freshly malloc'd buffer. Caller frees *buf. */ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) @@ -12388,6 +12434,13 @@ static int certmanLoadFile(const char* fn, byte** buf, word32* bufSz) return 0; } +#endif /* WOLFSSH_TEST_CERTMAN_PROMOTE || + * (WOLFSSH_TEST_SET_CERTMAN && WOLFSSH_TEST_CERTMAN_ROOTCA) || + * (WOLFSSH_TEST_INTERNAL && WOLFSSH_CERTS && !NO_FILESYSTEM && + * !WOLFSSH_NO_ECDSA_SHA2_NISTP256) */ + +#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, @@ -12801,37 +12854,54 @@ static int test_CertMan_PromoteValidCaIntermediate(void) #endif /* WOLFSSH_TEST_CERTMAN_PROMOTE */ -#ifdef WOLFSSH_CERTS +#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 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 (a reference leak there is only observable under the sanitizer or + * leak-checking builds, not by these return-value checks), 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; - - ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); - if (ctx == NULL) - result = -1; + WOLFSSL_CERT_MANAGER* cm2 = NULL; +#ifdef WOLFSSH_TEST_CERTMAN_ROOTCA + byte* root = NULL; + word32 rootSz = 0; + int haveRoot; + + /* 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. The + * skip is reported by returning 1 so it cannot pass as a full SUCCESS. */ + haveRoot = (certmanLoadFile("./keys/ca-cert-ecc.der", &root, &rootSz) + == 0); + if (!haveRoot) { + printf("SetCertManager: SKIP root cert checks, " + "./keys/ca-cert-ecc.der not readable\n"); + } +#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) @@ -12843,38 +12913,64 @@ 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 && haveRoot && + 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 && haveRoot && + 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 && haveRoot && 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); + if (result == 0 && !haveRoot) + result = 1; +#else + /* The reference-counting behaviour is only observable through the + * root-CA loads; without them this run verified the argument checks + * only, so report SKIPPED rather than a full SUCCESS. */ + if (result == 0) + result = 1; +#endif return result; } -#endif /* WOLFSSH_CERTS */ +#endif /* WOLFSSH_TEST_SET_CERTMAN */ #ifdef WOLFSSH_WINDOWS_CERT_STORE /* Check one wolfSSH_ParseCertStoreSpec call against expected results. @@ -12910,10 +13006,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; } @@ -12926,35 +13019,121 @@ static int test_ParseCertStoreSpec(void) wchar_t* wSubjectName = NULL; word32 dwFlags = 0; - /* bad arguments */ + /* bad arguments; the out pointers are seeded with a garbage sentinel so + * the documented "any non-NULL out-pointer is NULLed on failure" + * contract is actually observed, not just vacuously satisfied */ result = certStoreSpecCheck(NULL, WS_BAD_ARGUMENT, NULL, NULL, 0); + wSubjectName = (wchar_t*)(size_t)1; if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", NULL, &wSubjectName, &dwFlags, NULL) != WS_BAD_ARGUMENT) result = -10; + if (result == 0 && wSubjectName != NULL) + result = -13; + wStoreName = (wchar_t*)(size_t)1; if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, NULL, &dwFlags, NULL) != WS_BAD_ARGUMENT) result = -11; + if (result == 0 && wStoreName != NULL) + result = -14; + wStoreName = (wchar_t*)(size_t)1; + wSubjectName = (wchar_t*)(size_t)1; if (result == 0 && wolfSSH_ParseCertStoreSpec("My:server", &wStoreName, &wSubjectName, NULL, NULL) != WS_BAD_ARGUMENT) result = -12; + if (result == 0 && (wStoreName != NULL || wSubjectName != NULL)) + result = -15; + wStoreName = NULL; + wSubjectName = NULL; - /* full spec with named flag values */ + /* full spec with every named flag value, short form */ 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); + if (result == 0) + result = certStoreSpecCheck("My:server:USERS", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_USERS); + if (result == 0) + result = certStoreSpecCheck("My:server:CURRENT_SERVICE", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_CURRENT_SERVICE); + if (result == 0) + result = certStoreSpecCheck("My:server:SERVICES", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_SERVICES); + if (result == 0) + result = certStoreSpecCheck("My:server:CURRENT_USER_GROUP_POLICY", + WS_SUCCESS, L"My", L"server", + CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY); + if (result == 0) + result = certStoreSpecCheck("My:server:LOCAL_MACHINE_GROUP_POLICY", + WS_SUCCESS, L"My", L"server", + CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY); + if (result == 0) + result = certStoreSpecCheck("My:server:LOCAL_MACHINE_ENTERPRISE", + WS_SUCCESS, L"My", L"server", + CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE); + /* the long CERT_SYSTEM_STORE_* spellings are accepted too */ + if (result == 0) + result = certStoreSpecCheck( + "My:server:CERT_SYSTEM_STORE_LOCAL_MACHINE", WS_SUCCESS, + L"My", L"server", CERT_SYSTEM_STORE_LOCAL_MACHINE); + if (result == 0) + result = certStoreSpecCheck( + "My:server:CERT_SYSTEM_STORE_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 */ + /* a subject may carry the CN= prefix and a fourth field is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:CN=host:LOCAL_MACHINE", WS_SUCCESS, + L"My", L"CN=host", CERT_SYSTEM_STORE_LOCAL_MACHINE); + if (result == 0) + result = certStoreSpecCheck("My:server:CURRENT_USER:extra", + WS_BAD_ARGUMENT, NULL, NULL, 0); + + /* numeric flags: only system-store location bits are accepted */ + if (result == 0) + 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); + /* an unassigned location id (0x00030000) is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:0x00030000", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* an unrecognized name falls to the numeric parser and is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:BOGUS", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* strtoul()'s sign and whitespace forms must not sneak through */ + if (result == 0) + result = certStoreSpecCheck("My:server:+65536", WS_BAD_ARGUMENT, + NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:server:-0xFFFF0000", WS_BAD_ARGUMENT, + NULL, NULL, 0); + if (result == 0) + result = certStoreSpecCheck("My:server: 65536", WS_BAD_ARGUMENT, + NULL, NULL, 0); + /* trailing junk after the number is rejected */ + if (result == 0) + result = certStoreSpecCheck("My:server:65536x", WS_BAD_ARGUMENT, + NULL, NULL, 0); + + /* invalid UTF-8 in a name fails the wide conversion */ if (result == 0) - result = certStoreSpecCheck("My:server:12345", WS_SUCCESS, - L"My", L"server", 12345); + result = certStoreSpecCheck("My\xC3:server", WS_FATAL_ERROR, + NULL, NULL, 0); /* missing or empty fields are rejected */ if (result == 0) @@ -12971,8 +13150,56 @@ static int test_ParseCertStoreSpec(void) return result; } + + +/* Store-independent argument and no-side-effect checks for + * wolfSSH_CTX_UsePrivateKey_fromStore(): the NULL-argument and bad-dwFlags + * rejections run before any store is opened, and every failure must leave + * the context's key table untouched per the documented contract. */ +static int test_UsePrivateKeyFromStoreArgs(void) +{ + int result = 0; + WOLFSSH_CTX* ctx; + word32 keyCountBefore; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -1; + keyCountBefore = ctx->privateKeyCount; + + if (wolfSSH_CTX_UsePrivateKey_fromStore(NULL, L"My", + CERT_SYSTEM_STORE_CURRENT_USER, L"unit") != WS_BAD_ARGUMENT) + result = -2; + if (result == 0 && wolfSSH_CTX_UsePrivateKey_fromStore(ctx, NULL, + CERT_SYSTEM_STORE_CURRENT_USER, L"unit") != WS_BAD_ARGUMENT) + result = -3; + if (result == 0 && wolfSSH_CTX_UsePrivateKey_fromStore(ctx, L"My", + CERT_SYSTEM_STORE_CURRENT_USER, NULL) != WS_BAD_ARGUMENT) + result = -4; + /* zero and location-plus-control-flag values are not store locations */ + if (result == 0 && wolfSSH_CTX_UsePrivateKey_fromStore(ctx, L"My", 0, + L"unit") != WS_BAD_ARGUMENT) + result = -5; + if (result == 0 && wolfSSH_CTX_UsePrivateKey_fromStore(ctx, L"My", + 0x00010010, L"unit") != WS_BAD_ARGUMENT) + result = -6; + /* a store that cannot exist fails to open (read-only, open-existing) */ + if (result == 0 && wolfSSH_CTX_UsePrivateKey_fromStore(ctx, + L"wolfSSHUnitTestNoSuchStore", CERT_SYSTEM_STORE_CURRENT_USER, + L"unit") != WS_BAD_FILE_E) + result = -7; + /* on every failure the context is left unchanged */ + if (result == 0 && ctx->privateKeyCount != keyCountBefore) + result = -8; + + wolfSSH_CTX_free(ctx); + + return result; +} #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 @@ -14803,6 +15030,131 @@ static int test_ParseECCPubKey(void) return failures; } +#if defined(WOLFSSH_CERTS) && !defined(NO_FILESYSTEM) + +/* Big-endian word32 store; c32toa is WOLFSSH_LOCAL and not linkable here. */ +static void certChainPut32(word32 v, byte* c) +{ + c[0] = (byte)((v >> 24) & 0xFF); + c[1] = (byte)((v >> 16) & 0xFF); + c[2] = (byte)((v >> 8) & 0xFF); + c[3] = (byte)( v & 0xFF); +} + + +/* ParseECCPubKeyCert() Unit Test: the certificate host-key sibling of + * test_ParseECCPubKey(). Feeds an RFC 6187 chain blob wrapping + * keys/server-cert.der (a P-256 leaf chaining to keys/ca-cert-ecc.der) and + * checks the curve binding: the P-256 certificate must be accepted for + * x509v3-ecdsa-sha2-nistp256 and rejected with WS_INVALID_PRIME_CURVE for a + * negotiated x509v3-ecdsa-sha2-nistp384, and an id wcPrimeForId() cannot + * map must be rejected up front with WS_INVALID_PRIME_CURVE, matching + * ParseECCPubKey() (checked first: it needs no chain verification). + * Skipped (returns 1) when the key files are + * not readable or the chain does not verify in this build's profile (e.g. + * WOLFSSL_FPKI, whose leaf checks these test certs do not meet). */ +static int test_ParseECCPubKeyCert(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + byte* ca = NULL; + byte* cert = NULL; + byte* blob = NULL; + word32 caSz = 0, certSz = 0, blobSz = 0, idx; + int result = 0; + int ret; + static const char algoName[] = "x509v3-ecdsa-sha2-nistp256"; + + if (certmanLoadFile("./keys/ca-cert-ecc.der", &ca, &caSz) != 0 || + certmanLoadFile("./keys/server-cert.der", &cert, &certSz) + != 0) { + printf("ParseECCPubKeyCert: SKIP, ./keys certs not readable\n"); + free(ca); + free(cert); + return 1; + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_CLIENT, NULL); + if (ctx == NULL) + result = -1; + if (result == 0 && wolfSSH_CTX_AddRootCert_buffer(ctx, ca, caSz, + WOLFSSH_FORMAT_ASN1) != WS_SUCCESS) + result = -2; + if (result == 0) { + ssh = wolfSSH_new(ctx); + if (ssh == NULL || ssh->handshake == NULL) + result = -3; + } + + /* string name | uint32 cert count | string cert | uint32 ocsp count */ + if (result == 0) { + blobSz = LENGTH_SZ + (word32)WSTRLEN(algoName) + + UINT32_SZ + LENGTH_SZ + certSz + UINT32_SZ; + blob = (byte*)malloc(blobSz); + if (blob == NULL) + result = -4; + } + if (result == 0) { + idx = 0; + certChainPut32((word32)WSTRLEN(algoName), blob + idx); + idx += LENGTH_SZ; + WMEMCPY(blob + idx, algoName, WSTRLEN(algoName)); + idx += (word32)WSTRLEN(algoName); + certChainPut32(1, blob + idx); + idx += UINT32_SZ; + certChainPut32(certSz, blob + idx); + idx += LENGTH_SZ; + WMEMCPY(blob + idx, cert, certSz); + idx += certSz; + certChainPut32(0, blob + idx); + } + + /* an id with no curve mapping is rejected before the parse, so this + * works even in builds whose cert profile rejects the test chain */ + if (result == 0) { + ssh->handshake->pubKeyId = ID_SSH_RSA; + ret = wolfSSH_TestParseECCPubKeyCert(ssh, blob, blobSz); + if (ret != WS_INVALID_PRIME_CURVE) { + printf("ParseECCPubKeyCert: unmapped id ret %d\n", ret); + result = -7; + } + } + + /* matching curve accepted; a failure here means the chain itself did + * not verify under this build's profile, so skip the curve vectors + * rather than fail on unrelated policy */ + if (result == 0) { + ssh->handshake->pubKeyId = ID_X509V3_ECDSA_SHA2_NISTP256; + ret = wolfSSH_TestParseECCPubKeyCert(ssh, blob, blobSz); + if (ret != WS_SUCCESS) { + printf("ParseECCPubKeyCert: control chain did not verify " + "(ret %d), skipping curve vectors\n", ret); + result = 1; + } + } +#ifndef WOLFSSH_NO_ECDSA_SHA2_NISTP384 + /* P-256 leaf offered for a negotiated P-384 algorithm is rejected */ + if (result == 0) { + ssh->handshake->pubKeyId = ID_X509V3_ECDSA_SHA2_NISTP384; + ret = wolfSSH_TestParseECCPubKeyCert(ssh, blob, blobSz); + if (ret != WS_INVALID_PRIME_CURVE) { + printf("ParseECCPubKeyCert: curve mismatch ret %d\n", ret); + result = -6; + } + } +#endif + + free(blob); + free(cert); + free(ca); + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + + return result; +} + +#endif /* WOLFSSH_CERTS && !NO_FILESYSTEM */ + #endif /* !WOLFSSH_NO_ECDSA_SHA2_NISTP256 */ @@ -16632,6 +16984,13 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("ParseECCPubKey: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#if defined(WOLFSSH_CERTS) && !defined(NO_FILESYSTEM) + unitResult = test_ParseECCPubKeyCert(); + /* 1 means the ./keys certs were unreadable or unverifiable here */ + printf("ParseECCPubKeyCert: %s\n", (unitResult == 0 ? "SUCCESS" : + unitResult > 0 ? "SKIPPED" : "FAILED")); + testResult = testResult || (unitResult < 0); +#endif #endif #if !defined(WOLFSSH_NO_ED25519) && defined(HAVE_ED25519) && \ defined(HAVE_ED25519_SIGN) && defined(HAVE_ED25519_VERIFY) && \ @@ -16890,17 +17249,24 @@ int wolfSSH_UnitTest(int argc, char** argv) #endif -#if defined(WOLFSSH_TEST_INTERNAL) && defined(WOLFSSH_CERTS) +#ifdef WOLFSSH_TEST_SET_CERTMAN unitResult = test_SetCertManager(); - printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); - testResult = testResult || unitResult; + /* 1 means the argument checks passed but the root-CA half was skipped */ + printf("SetCertManager: %s\n", (unitResult == 0 ? "SUCCESS" : + unitResult > 0 ? "SKIPPED (root CA checks)" : "FAILED")); + testResult = testResult || (unitResult < 0); #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")); testResult = testResult || unitResult; + + unitResult = test_UsePrivateKeyFromStoreArgs(); + printf("UsePrivateKeyFromStoreArgs: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; #endif #ifdef WOLFSSH_TEST_CERTMAN_PROMOTE diff --git a/wolfssh/certman.h b/wolfssh/certman.h index fe68aeaf5..07169cddd 100644 --- a/wolfssh/certman.h +++ b/wolfssh/certman.h @@ -30,8 +30,12 @@ #include #include -#include /* included for WOLFSSH_CTX */ -#include /* included for WOLFSSL_CERT_MANAGER struct */ +#ifdef WOLFSSH_CERTS + #include /* included for WOLFSSH_CTX */ +#endif +#ifdef WOLFSSH_WINDOWS_CERT_STORE + #include +#endif #ifdef __cplusplus extern "C" { @@ -42,8 +46,27 @@ struct WOLFSSH_CERTMAN; typedef struct WOLFSSH_CERTMAN WOLFSSH_CERTMAN; +#ifdef WOLFSSH_CERTS +/* Only a pointer to the wolfSSL cert manager is named here, so a forward + * struct reference keeps out of every translation unit that + * includes wolfssh/internal.h; src/certman.c includes the real header. */ +struct WOLFSSL_CERT_MANAGER; + +/* Replaces the CTX's cert manager with cm, taking a reference on it and + * applying wolfSSH's revocation policy. The caller retains ownership, but + * note wolfSSH mutates 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, + * and during certificate authentication wolfSSH permanently adds verified + * peer intermediate CAs to the manager as trusted roots. Prefer a manager + * dedicated to wolfSSH over one shared with a live TLS stack. On failure + * (WS_FATAL_ERROR) nothing has changed: the CTX keeps its previous manager + * and no policy has been applied to cm. 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); +int wolfSSH_SetCertManager(WOLFSSH_CTX* ctx, struct WOLFSSL_CERT_MANAGER* cm); +#endif /* WOLFSSH_CERTS */ WOLFSSH_API WOLFSSH_CERTMAN* wolfSSH_CERTMAN_new(void* heap); @@ -60,12 +83,37 @@ 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) +/* Parses a Windows system-store location name into its CERT_SYSTEM_STORE_* + * value. Accepts the short names CURRENT_USER, LOCAL_MACHINE, USERS, + * CURRENT_SERVICE, SERVICES, CURRENT_USER_GROUP_POLICY, + * LOCAL_MACHINE_GROUP_POLICY and LOCAL_MACHINE_ENTERPRISE, the same names + * with a CERT_SYSTEM_STORE_ prefix, or a decimal/0x-hex number consumed + * whole (a leading sign or whitespace is rejected). Only assigned store + * locations are accepted, never control flags. Returns WS_SUCCESS on + * success. */ +WOLFSSH_API +int wolfSSH_CertStoreLocationFromName(const char* in, word32* out); + +/* Splits "store:subject[:flags]", where flags takes any spelling + * wolfSSH_CertStoreLocationFromName() accepts 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. 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 any non-NULL out-pointer is 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); -#endif /* WOLFSSH_WINDOWS_CERT_STORE */ + +/* 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 */ #ifdef __cplusplus diff --git a/wolfssh/internal.h b/wolfssh/internal.h index afc076658..e8dee93e1 100644 --- a/wolfssh/internal.h +++ b/wolfssh/internal.h @@ -64,8 +64,8 @@ #ifndef WOLFSSH_CERTS #error "WOLFSSH_WINDOWS_CERT_STORE requires WOLFSSH_CERTS" #endif - #ifndef _WIN32 - #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows (_WIN32) target" + #if !defined(_WIN32) && !defined(USE_WINDOWS_API) + #error "WOLFSSH_WINDOWS_CERT_STORE requires a Windows target" #endif #endif /* WOLFSSH_WINDOWS_CERT_STORE */ @@ -118,6 +118,8 @@ extern "C" { #define WOLFSSH_NO_DH #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 @@ -779,22 +781,20 @@ 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; +#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 { @@ -1098,6 +1098,10 @@ struct WOLFSSH { byte connReset; byte isClosed; byte clientOpenSSH; +#ifdef WOLFSSH_FWD + byte fwdCbMissingWarned; /* one-shot: missing fwdCb warned this session */ +#endif + byte chanOpenCbMissingWarned; /* one-shot: missing channelOpenCb warned */ byte kexId; byte blockSz; @@ -1605,6 +1609,9 @@ WOLFSSH_LOCAL enum wc_HashType HashForId(byte id); #ifdef WOLFSSH_CERTS WOLFSSH_LOCAL byte CertTypeForId(byte id); #endif +#if defined(WOLFSSH_WINDOWS_CERT_STORE) && defined(WOLFSSH_CERTS) +WOLFSSH_LOCAL word32 FindPvtKeyIdx(const WOLFSSH_CTX* ctx, byte fmt); +#endif enum AcceptStates { @@ -1906,6 +1913,10 @@ enum WS_MessageIdLimits { #ifndef WOLFSSH_NO_ECDSA WOLFSSH_API int wolfSSH_TestParseECCPubKey(WOLFSSH* ssh, byte* pubKey, word32 pubKeySz); +#ifdef WOLFSSH_CERTS + WOLFSSH_API int wolfSSH_TestParseECCPubKeyCert(WOLFSSH* ssh, byte* pubKey, + word32 pubKeySz); +#endif /* WOLFSSH_CERTS */ #endif /* !WOLFSSH_NO_ECDSA */ #ifndef WOLFSSH_NO_ED25519 WOLFSSH_API int wolfSSH_TestParseEd25519PubKey(WOLFSSH* ssh, byte* pubKey, diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index a206b4368..070cd30da 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -308,6 +308,10 @@ WOLFSSH_API int wolfSSH_ChannelIsPty(const WOLFSSH_CHANNEL* channel); /* Channel callbacks */ typedef int (*WS_CallbackChannelOpen)(WOLFSSH_CHANNEL* channel, void* ctx); +/* Policy callback for peer channel open requests. NOTE: when no callback + * is registered every channel open from the peer is ACCEPTED by default + * (except forwarding channel types, which fail closed without a forward + * callback); register one to enforce a channel policy. */ WOLFSSH_API int wolfSSH_CTX_SetChannelOpenCb(WOLFSSH_CTX* ctx, WS_CallbackChannelOpen cb); WOLFSSH_API int wolfSSH_CTX_SetChannelOpenRespCb(WOLFSSH_CTX* ctx, @@ -557,9 +561,47 @@ WOLFSSH_API int wolfSSH_CTX_UsePrivateKey_buffer(WOLFSSH_CTX* ctx, const char* name); #endif #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. When no time-valid certificate with a usable + * private key matches, the call fails with WS_CERT_EXPIRED_E; define + * WOLFSSH_CERT_STORE_ALLOW_EXPIRED to instead select an expired or + * not-yet-valid certificate with a usable key, preferring expired over + * not yet valid and then the latest NotAfter (the fallback is logged, + * but only in builds with logging compiled in, DEBUG_WOLFSSH or + * WOLFSSH_SSHD). A mixed configuration -- a file- or TPM-based host + * key or host certificate already loaded for the same algorithm -- is + * rejected with WS_BAD_ARGUMENT in both load orders; replacing a + * previously loaded store key is allowed. + * Returns WS_SUCCESS on success, WS_BAD_ARGUMENT on a NULL argument, + * bad dwFlags, or an unsupported/mixed key configuration, WS_BAD_FILE_E + * when the store cannot be opened, WS_CRYPTO_FAILED when certificates + * match but none has a private key that is both accessible (check key + * permissions for the service account) and enrolled for signing + * (the log distinguishes the two), WS_CERT_EXPIRED_E when only certificates + * outside their validity period match (see above), WS_CTX_KEY_COUNT_E + * when two key slots are not free, WS_MEMORY_E on an allocation + * failure, 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); + /* Report the certificate a loaded cert-store host key is bound to, so + * an application can offer it for certificate user auth. cert/certSz + * point at DER owned by the CTX and algoName at the static x509v3 + * algorithm name; each out pointer may be NULL. When several store + * credentials are loaded, the first x509v3 slot in load order is + * returned -- there is no per-algorithm selector, so only one store + * credential can be offered and the load order decides which. Returns + * WS_SUCCESS, WS_BAD_ARGUMENT on a NULL ctx, or WS_FATAL_ERROR when no + * cert-store-backed x509v3 slot exists. */ + WOLFSSH_API int wolfSSH_CTX_GetCertStoreCert(WOLFSSH_CTX* ctx, + const byte** cert, word32* certSz, const char** algoName); #endif #endif /* WOLFSSH_CERTS */ WOLFSSH_API int wolfSSH_CTX_SetWindowPacketSize(WOLFSSH_CTX* ctx, diff --git a/wolfssh/test.h b/wolfssh/test.h index ebeefc52f..7da657318 100644 --- a/wolfssh/test.h +++ b/wolfssh/test.h @@ -401,7 +401,7 @@ static INLINE int mygetopt(int argc, char** argv, const char* optstring) } -#ifdef USE_WINDOWS_API +#if defined(USE_WINDOWS_API) && defined(_MSC_VER) #pragma warning(push) #pragma warning(disable:4996) /* For Windows builds, disable compiler warnings for: @@ -563,7 +563,7 @@ static INLINE void build_addr(SOCKADDR_IN_T* addr, const char* peer, } #endif /* WOLFSSH_NUCLEUS */ -#ifdef USE_WINDOWS_API +#if defined(USE_WINDOWS_API) && defined(_MSC_VER) #pragma warning(pop) #endif @@ -1158,31 +1158,45 @@ static INLINE void build_addr_ipv6(struct sockaddr_in6* addr, const char* peer, #ifdef WOLFSSH_TEST_HEX2BIN -/* Declares Base16_Decode when wolfSSL has it, and settles WOLFSSL_BASE16 - * for the guard below. Only --enable-base16 and the options that imply it - * (openssh, sm2, all) build it, so the local copy is still needed. */ +/* Included unconditionally, and before the fallback below defines the + * Base16_Decode macro: a later transitive include of coding.h (e.g. via + * wolfssl/ssl.h in OPENSSL_EXTRA builds) is then an include-guard no-op + * instead of having its Base16_Decode declaration rewritten by the macro + * into a conflicting WS_Base16_Decode declaration. */ #include -#ifndef WOLFSSL_BASE16 +/* Use the local fallback whenever wolfSSL will not supply Base16_Decode: + * only --enable-base16 and the options that imply it (openssh, sm2, all) + * build it, and coding.c compiles to nothing under NO_CODING even with + * WOLFSSL_BASE16 set. The fallback has a private name mapped onto + * Base16_Decode so it can never collide with coding.h's declaration, which + * is present under WOLFSSL_BASE16 even when NO_CODING empties the + * implementation, or with its prefix map. */ +#if !defined(WOLFSSL_BASE16) || defined(NO_CODING) -#define BAD 0xFF +#define WS_HEX_BAD 0xFF -#ifndef WOLFSSL_BASE16 static const byte hexDecode[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, - BAD, BAD, BAD, BAD, BAD, BAD, BAD, + WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, 10, 11, 12, 13, 14, 15, /* upper case A-F */ - BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, - BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, - BAD, BAD, BAD, BAD, BAD, BAD, BAD, BAD, - BAD, BAD, /* G - ` */ + WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, + WS_HEX_BAD, WS_HEX_BAD, /* G - ` */ 10, 11, 12, 13, 14, 15 /* lower case a-f */ }; /* A starts at 0x41 not 0x3A */ +#undef Base16_Decode +#define Base16_Decode WS_Base16_Decode -static int Base16_Decode(const byte* in, word32 inLen, - byte* out, word32* outLen) +static int WS_Base16_Decode(const byte* in, word32 inLen, + byte* out, word32* outLen) { word32 inIdx = 0; word32 outIdx = 0; @@ -1199,7 +1213,7 @@ static int Base16_Decode(const byte* in, word32 inLen, b = hexDecode[b]; - if (b == BAD) + if (b == WS_HEX_BAD) return -1; out[outIdx++] = b; @@ -1227,7 +1241,7 @@ static int Base16_Decode(const byte* in, word32 inLen, b = hexDecode[b]; b2 = hexDecode[b2]; - if (b == BAD || b2 == BAD) + if (b == WS_HEX_BAD || b2 == WS_HEX_BAD) return -1; out[outIdx++] = (byte)((b << 4) | b2); @@ -1237,7 +1251,10 @@ static int Base16_Decode(const byte* in, word32 inLen, *outLen = outIdx; return 0; } -#endif /* !WOLFSSL_BASE16 */ + +#undef WS_HEX_BAD + +#endif /* !WOLFSSL_BASE16 || NO_CODING */ static void FreeBins(byte* b1, byte* b2, byte* b3, byte* b4) {