From 6f5fa855107ea6f4c6fb689aaa516da652b21a8b Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Thu, 2 Apr 2026 10:09:01 -0700 Subject: [PATCH 1/2] Initial implementation of SSHKDF for wolfprovider --- include/wolfprovider/alg_funcs.h | 2 + include/wolfprovider/settings.h | 3 + include/wolfprovider/wp_logging.h | 4 +- src/include.am | 1 + src/wp_logging.c | 1 + src/wp_sshkdf.c | 395 ++++++++++++++++++++++ src/wp_wolfprov.c | 4 + test/include.am | 1 + test/test_sshkdf.c | 521 ++++++++++++++++++++++++++++++ test/unit.c | 3 + test/unit.h | 3 + 11 files changed, 937 insertions(+), 1 deletion(-) create mode 100644 src/wp_sshkdf.c create mode 100644 test/test_sshkdf.c diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index 22345b16..6e9bd1af 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -149,6 +149,7 @@ typedef void (*DFUNC)(void); #define WP_NAMES_TLS1_PRF "TLS1-PRF" #define WP_NAMES_KBKDF "KBKDF" #define WP_NAMES_KRB5KDF "KRB5KDF" +#define WP_NAMES_SSHKDF "SSHKDF" /* Signature names. */ #define WP_NAMES_RSA "RSA:rsaEncryption:1.2.840.113549.1.1.1" @@ -315,6 +316,7 @@ extern const OSSL_DISPATCH wp_kdf_tls1_3_kdf_functions[]; extern const OSSL_DISPATCH wp_kdf_tls1_prf_functions[]; extern const OSSL_DISPATCH wp_kdf_kbkdf_functions[]; extern const OSSL_DISPATCH wp_kdf_krb5kdf_functions[]; +extern const OSSL_DISPATCH wp_kdf_sshkdf_functions[]; /* Signature implementations. */ extern const OSSL_DISPATCH wp_rsa_signature_functions[]; diff --git a/include/wolfprovider/settings.h b/include/wolfprovider/settings.h index fffd1abc..151bc707 100644 --- a/include/wolfprovider/settings.h +++ b/include/wolfprovider/settings.h @@ -116,6 +116,9 @@ #ifndef NO_PWDBASED #define WP_HAVE_PBE #endif +#ifdef WOLFSSL_WOLFSSH + #define WP_HAVE_SSHKDF +#endif #ifndef NO_DH #define WP_HAVE_DH diff --git a/include/wolfprovider/wp_logging.h b/include/wolfprovider/wp_logging.h index b026c1ba..18cab941 100644 --- a/include/wolfprovider/wp_logging.h +++ b/include/wolfprovider/wp_logging.h @@ -186,6 +186,7 @@ #define WP_LOG_COMP_X448 0x4000000 /* X448 operations */ #define WP_LOG_COMP_QUERY 0x8000000 /* wolfprov_query operations */ #define WP_LOG_COMP_TLS1_PRF 0x10000000 /* TLS1 PRF operations */ +#define WP_LOG_COMP_SSHKDF 0x20000000 /* SSHKDF operations */ /* log all components */ #define WP_LOG_COMP_ALL ( \ @@ -217,7 +218,8 @@ WP_LOG_COMP_X25519 | \ WP_LOG_COMP_X448 | \ WP_LOG_COMP_QUERY | \ - WP_LOG_COMP_TLS1_PRF ) + WP_LOG_COMP_TLS1_PRF | \ + WP_LOG_COMP_SSHKDF ) /* default components logged */ #define WP_LOG_COMP_DEFAULT WP_LOG_COMP_ALL diff --git a/src/include.am b/src/include.am index 8c580401..5d8db01b 100644 --- a/src/include.am +++ b/src/include.am @@ -23,6 +23,7 @@ libwolfprov_la_SOURCES += src/wp_kdf_exch.c libwolfprov_la_SOURCES += src/wp_pbkdf2.c libwolfprov_la_SOURCES += src/wp_kbkdf.c libwolfprov_la_SOURCES += src/wp_krb5kdf.c +libwolfprov_la_SOURCES += src/wp_sshkdf.c libwolfprov_la_SOURCES += src/wp_rsa_kmgmt.c libwolfprov_la_SOURCES += src/wp_rsa_sig.c libwolfprov_la_SOURCES += src/wp_rsa_asym.c diff --git a/src/wp_logging.c b/src/wp_logging.c index c7e69de6..bc526b1d 100644 --- a/src/wp_logging.c +++ b/src/wp_logging.c @@ -531,6 +531,7 @@ static void wolfProv_LogComponentToMask(const char* level, size_t len, void* ctx { "WP_LOG_COMP_X448", XSTRLEN("WP_LOG_COMP_X448"), WP_LOG_COMP_X448 }, { "WP_LOG_COMP_QUERY", XSTRLEN("WP_LOG_COMP_QUERY"), WP_LOG_COMP_QUERY }, { "WP_LOG_COMP_TLS1_PRF", XSTRLEN("WP_LOG_COMP_TLS1_PRF"), WP_LOG_COMP_TLS1_PRF }, + { "WP_LOG_COMP_SSHKDF", XSTRLEN("WP_LOG_COMP_SSHKDF"), WP_LOG_COMP_SSHKDF }, { "WP_LOG_COMP_ALL", XSTRLEN("WP_LOG_COMP_ALL"), WP_LOG_COMP_ALL }, diff --git a/src/wp_sshkdf.c b/src/wp_sshkdf.c new file mode 100644 index 00000000..8c2fca94 --- /dev/null +++ b/src/wp_sshkdf.c @@ -0,0 +1,395 @@ +/* wp_sshkdf.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifdef WP_HAVE_SSHKDF + +/** + * The SSHKDF context structure. + */ +typedef struct wp_SshkdfCtx { + /** wolfSSL provider context. */ + WOLFPROV_CTX* provCtx; + + /** Hash type for SSHKDF. */ + enum wc_HashType mdType; + /** Length of digest output in bytes. */ + size_t mdLen; + + /** Shared secret K. */ + unsigned char* key; + /** Size of shared secret in bytes. */ + size_t keySz; + + /** Exchange hash H. */ + unsigned char* xcghash; + /** Size of exchange hash in bytes. */ + size_t xcghashSz; + + /** Session ID. */ + unsigned char* sessionId; + /** Size of session ID in bytes. */ + size_t sessionIdSz; + + /** Key type character ('A'-'F'). */ + char type; +} wp_SshkdfCtx; + +/** + * Create a new SSHKDF context object. + * + * @param [in] provCtx wolfProvider context object. + * @return NULL on failure. + * @return SSHKDF context object on success. + */ +static wp_SshkdfCtx* wp_kdf_sshkdf_new(WOLFPROV_CTX* provCtx) +{ + wp_SshkdfCtx* ctx = NULL; + + if (wolfssl_prov_is_running()) { + ctx = OPENSSL_zalloc(sizeof(*ctx)); + } + if (ctx != NULL) { + ctx->provCtx = provCtx; + } + + return ctx; +} + +/** + * Clear SSHKDF context object. + * + * @param [in, out] ctx SSHKDF context object. + */ +static void wp_kdf_sshkdf_clear(wp_SshkdfCtx* ctx) +{ + if (ctx != NULL) { + OPENSSL_clear_free(ctx->key, ctx->keySz); + OPENSSL_clear_free(ctx->xcghash, ctx->xcghashSz); + OPENSSL_clear_free(ctx->sessionId, ctx->sessionIdSz); + } +} + +/** + * Free the SSHKDF context object. + * + * @param [in, out] ctx SSHKDF context object. + */ +static void wp_kdf_sshkdf_free(wp_SshkdfCtx* ctx) +{ + if (ctx != NULL) { + wp_kdf_sshkdf_clear(ctx); + OPENSSL_free(ctx); + } +} + +/** + * Reset SSHKDF context object. + * + * Disposes of allocated data. + * + * @param [in, out] ctx SSHKDF context object. + */ +static void wp_kdf_sshkdf_reset(wp_SshkdfCtx* ctx) +{ + if (ctx != NULL) { + WOLFPROV_CTX* provCtx = ctx->provCtx; + wp_kdf_sshkdf_clear(ctx); + XMEMSET(ctx, 0, sizeof(*ctx)); + ctx->provCtx = provCtx; + } +} + +/** + * Set the SSHKDF context parameters. + * + * @param [in, out] ctx SSHKDF context object. + * @param [in] params Array of parameters with values. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_kdf_sshkdf_set_ctx_params(wp_SshkdfCtx* ctx, + const OSSL_PARAM params[]) +{ + int ok = 1; + const OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_SSHKDF, "wp_kdf_sshkdf_set_ctx_params"); + + if (params != NULL) { + /* Get digest/hash type. */ + if (ok && !wp_params_get_digest(params, NULL, + ctx->provCtx->libCtx, &ctx->mdType, &ctx->mdLen)) { + ok = 0; + } + + /* Get shared secret K. */ + if (ok) { + p = OSSL_PARAM_locate((OSSL_PARAM*)params, OSSL_KDF_PARAM_KEY); + if ((p != NULL) && (p->data != NULL)) { + OPENSSL_clear_free(ctx->key, ctx->keySz); + ctx->key = NULL; + ctx->keySz = 0; + if (!OSSL_PARAM_get_octet_string(p, (void**)&ctx->key, 0, + &ctx->keySz)) { + ok = 0; + } + } + } + + /* Get exchange hash H. */ + if (ok) { + p = OSSL_PARAM_locate((OSSL_PARAM*)params, + OSSL_KDF_PARAM_SSHKDF_XCGHASH); + if ((p != NULL) && (p->data != NULL)) { + OPENSSL_clear_free(ctx->xcghash, ctx->xcghashSz); + ctx->xcghash = NULL; + ctx->xcghashSz = 0; + if (!OSSL_PARAM_get_octet_string(p, (void**)&ctx->xcghash, 0, + &ctx->xcghashSz)) { + ok = 0; + } + } + } + + /* Get session ID. */ + if (ok) { + p = OSSL_PARAM_locate((OSSL_PARAM*)params, + OSSL_KDF_PARAM_SSHKDF_SESSION_ID); + if ((p != NULL) && (p->data != NULL)) { + OPENSSL_clear_free(ctx->sessionId, ctx->sessionIdSz); + ctx->sessionId = NULL; + ctx->sessionIdSz = 0; + if (!OSSL_PARAM_get_octet_string(p, (void**)&ctx->sessionId, 0, + &ctx->sessionIdSz)) { + ok = 0; + } + } + } + + /* Get key type character ('A'-'F'). */ + if (ok) { + p = OSSL_PARAM_locate((OSSL_PARAM*)params, + OSSL_KDF_PARAM_SSHKDF_TYPE); + if (p != NULL) { + const char* kdftype = NULL; + + if (!OSSL_PARAM_get_utf8_string_ptr(p, &kdftype)) { + ok = 0; + } + else if (kdftype == NULL || p->data_size != 1) { + ok = 0; + } + else if (kdftype[0] < 'A' || kdftype[0] > 'F') { + ok = 0; + } + else { + ctx->type = kdftype[0]; + } + } + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_SSHKDF, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Get the SSHKDF context parameters. + * + * @param [in] ctx SSHKDF context object. + * @param [in, out] params Array of parameters with values. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_kdf_sshkdf_get_ctx_params(wp_SshkdfCtx* ctx, + OSSL_PARAM params[]) +{ + int ok = 1; + OSSL_PARAM* p; + + WOLFPROV_ENTER(WP_LOG_COMP_SSHKDF, "wp_kdf_sshkdf_get_ctx_params"); + + (void)ctx; + + p = OSSL_PARAM_locate(params, OSSL_KDF_PARAM_SIZE); + if (p != NULL) { + if (!OSSL_PARAM_set_size_t(p, SIZE_MAX)) { + ok = 0; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_SSHKDF, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); + return ok; +} + +/** + * Returns the parameters that can be set in the SSHKDF context. + * + * @param [in] ctx SSHKDF context object. Unused. + * @param [in] provCtx Provider context object. Unused. + * @return Array of parameters. + */ +static const OSSL_PARAM* wp_kdf_sshkdf_settable_ctx_params(wp_SshkdfCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_sshkdf_supported_settable_ctx_params[] = { + OSSL_PARAM_utf8_string(OSSL_KDF_PARAM_PROPERTIES, NULL, 0), + OSSL_PARAM_utf8_string(OSSL_KDF_PARAM_DIGEST, NULL, 0), + OSSL_PARAM_octet_string(OSSL_KDF_PARAM_KEY, NULL, 0), + OSSL_PARAM_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, NULL, 0), + OSSL_PARAM_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, NULL, 0), + OSSL_PARAM_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, NULL, 0), + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_sshkdf_supported_settable_ctx_params; +} + +/** + * Returns the parameters that can be retrieved from the SSHKDF context. + * + * @param [in] ctx SSHKDF context object. Unused. + * @param [in] provCtx Provider context object. Unused. + * @return Array of parameters. + */ +static const OSSL_PARAM* wp_kdf_sshkdf_gettable_ctx_params(wp_SshkdfCtx* ctx, + WOLFPROV_CTX* provCtx) +{ + static const OSSL_PARAM wp_sshkdf_supported_gettable_ctx_params[] = { + OSSL_PARAM_size_t(OSSL_KDF_PARAM_SIZE, NULL), + OSSL_PARAM_END + }; + (void)ctx; + (void)provCtx; + return wp_sshkdf_supported_gettable_ctx_params; +} + +/** + * Derive a key using SSHKDF (RFC 4253, Section 7.2). + * + * @param [in, out] ctx SSHKDF context object. + * @param [out] key Buffer to hold derived key. + * @param [in] keyLen Size of buffer in bytes. + * @param [in] params Array of parameters to set before deriving. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_kdf_sshkdf_derive(wp_SshkdfCtx* ctx, unsigned char* key, + size_t keyLen, const OSSL_PARAM params[]) +{ + int ok = 1; + int rc; + const unsigned char* rawKey; + word32 rawKeySz; + + WOLFPROV_ENTER(WP_LOG_COMP_SSHKDF, "wp_kdf_sshkdf_derive"); + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + if (ok && (!wp_kdf_sshkdf_set_ctx_params(ctx, params))) { + ok = 0; + } + if (ok && (ctx->mdType == WC_HASH_TYPE_NONE)) { + ok = 0; + } + if (ok && (ctx->key == NULL)) { + ok = 0; + } + if (ok && (ctx->xcghash == NULL)) { + ok = 0; + } + if (ok && (ctx->sessionId == NULL)) { + ok = 0; + } + if (ok && (ctx->type == 0)) { + ok = 0; + } + if (ok && (keyLen > UINT32_MAX || ctx->keySz > UINT32_MAX || + ctx->xcghashSz > UINT32_MAX || ctx->sessionIdSz > UINT32_MAX)) { + ok = 0; + } + + if (ok) { + rawKey = ctx->key; + rawKeySz = (word32)ctx->keySz; + + /* The caller passes K in SSH mpint encoding (4-byte big-endian length + * prefix, optional 0x00 padding byte, then value). wc_SSH_KDF() adds + * its own mpint encoding internally, so strip the caller's encoding + * to avoid double-encoding. */ + if (rawKeySz >= 4) { + word32 mpintLen = ((word32)rawKey[0] << 24) | + ((word32)rawKey[1] << 16) | + ((word32)rawKey[2] << 8) | + (word32)rawKey[3]; + if (mpintLen + 4 == rawKeySz && mpintLen > 0) { + rawKey += 4; + rawKeySz -= 4; + /* Skip leading 0x00 padding if present (next byte has MSB + * set, indicating the padding was added for sign extension). */ + if (rawKeySz > 1 && rawKey[0] == 0x00 && + (rawKey[1] & 0x80)) { + rawKey += 1; + rawKeySz -= 1; + } + } + } + + rc = wc_SSH_KDF((byte)ctx->mdType, (byte)ctx->type, + key, (word32)keyLen, + rawKey, rawKeySz, + ctx->xcghash, (word32)ctx->xcghashSz, + ctx->sessionId, (word32)ctx->sessionIdSz); + if (rc != 0) { + WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_SSHKDF, "wc_SSH_KDF", rc); + ok = 0; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_SSHKDF, "wp_kdf_sshkdf_derive", ok); + return ok; +} + +/** Dispatch table for SSHKDF functions implemented using wolfSSL. */ +const OSSL_DISPATCH wp_kdf_sshkdf_functions[] = { + { OSSL_FUNC_KDF_NEWCTX, (DFUNC)wp_kdf_sshkdf_new }, + { OSSL_FUNC_KDF_FREECTX, (DFUNC)wp_kdf_sshkdf_free }, + { OSSL_FUNC_KDF_RESET, (DFUNC)wp_kdf_sshkdf_reset }, + { OSSL_FUNC_KDF_DERIVE, (DFUNC)wp_kdf_sshkdf_derive }, + { OSSL_FUNC_KDF_SETTABLE_CTX_PARAMS, (DFUNC)wp_kdf_sshkdf_settable_ctx_params }, + { OSSL_FUNC_KDF_SET_CTX_PARAMS, (DFUNC)wp_kdf_sshkdf_set_ctx_params }, + { OSSL_FUNC_KDF_GETTABLE_CTX_PARAMS, (DFUNC)wp_kdf_sshkdf_gettable_ctx_params }, + { OSSL_FUNC_KDF_GET_CTX_PARAMS, (DFUNC)wp_kdf_sshkdf_get_ctx_params }, + { 0, NULL } +}; + +#endif /* WP_HAVE_SSHKDF */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index 1b806bab..099d9290 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -583,6 +583,10 @@ static const OSSL_ALGORITHM wolfprov_kdfs[] = { { WP_NAMES_KRB5KDF, WOLFPROV_PROPERTIES, wp_kdf_krb5kdf_functions, "" }, #endif +#ifdef WP_HAVE_SSHKDF + { WP_NAMES_SSHKDF, WOLFPROV_PROPERTIES, wp_kdf_sshkdf_functions, + "" }, +#endif { NULL, NULL, NULL, NULL } }; diff --git a/test/include.am b/test/include.am index 9651d250..a660b3e5 100644 --- a/test/include.am +++ b/test/include.am @@ -22,6 +22,7 @@ test_unit_test_SOURCES = \ test/test_ecx.c \ test/test_gmac.c \ test/test_krb5kdf.c \ + test/test_sshkdf.c \ test/test_hkdf.c \ test/test_hmac.c \ test/test_kbkdf.c \ diff --git a/test/test_sshkdf.c b/test/test_sshkdf.c new file mode 100644 index 00000000..f3f5b1db --- /dev/null +++ b/test/test_sshkdf.c @@ -0,0 +1,521 @@ +/* test_sshkdf.c + * + * Copyright (C) 2006-2026 wolfSSL Inc. + * + * This file is part of wolfProvider. + * + * wolfProvider is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfProvider is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with wolfProvider. If not, see . + */ + +#include "unit.h" + +#ifdef WP_HAVE_SSHKDF + +/* Helper to derive using SSHKDF via the EVP_KDF API. */ +static int test_sshkdf_calc(OSSL_LIB_CTX* libCtx, unsigned char *key, + size_t keyLen, const char* digest, const unsigned char* inKey, + size_t inKeyLen, const unsigned char* xcghash, size_t xcghashLen, + const unsigned char* sessionId, size_t sessionIdLen, const char* type) +{ + int err = 0; + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[7], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("Failed to fetch SSHKDF"); + err = 1; + } + + if (err == 0) { + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + PRINT_MSG("Failed to create KDF context"); + err = 1; + } + } + + if (err == 0) { + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)digest, 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, + (unsigned char*)inKey, inKeyLen); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, + (unsigned char*)xcghash, xcghashLen); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, + (unsigned char*)sessionId, sessionIdLen); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)type, 1); + *p = OSSL_PARAM_construct_end(); + + if (EVP_KDF_derive(kctx, key, keyLen, params) <= 0) { + PRINT_MSG("Failed to derive key"); + err = 1; + } + } + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + return err; +} + +/* Test SSHKDF vectors - compare wolfProvider output against OpenSSL default. */ +static int test_sshkdf_vector(void) +{ + int err = 0; + int i; + unsigned char oKey[64]; + unsigned char wKey[64]; + + /* Test input shared secret K. */ + unsigned char inKey[] = { + 0x00, 0x00, 0x00, 0x80, + 0x55, 0xba, 0xe9, 0x31, 0xc0, 0x7f, 0xd8, 0x24, + 0xbf, 0x10, 0xad, 0xd1, 0x90, 0x2b, 0x6f, 0xbc, + 0x7c, 0x66, 0x4b, 0xf2, 0xd7, 0x51, 0x0f, 0x88, + 0x9e, 0x2c, 0x31, 0xe7, 0xf5, 0x6a, 0x9b, 0x74, + 0x12, 0x42, 0x53, 0x07, 0x42, 0x02, 0x4f, 0x30, + 0xaa, 0x2f, 0x43, 0x12, 0x95, 0x6a, 0x71, 0x1b, + 0x1a, 0x01, 0x95, 0x6f, 0xd2, 0x18, 0x37, 0x6b, + 0x7e, 0x5f, 0x29, 0x3a, 0x3a, 0xa7, 0xf0, 0x8c, + 0xe5, 0x0f, 0x9a, 0x75, 0x79, 0xcd, 0x2e, 0x43, + 0x1b, 0x3b, 0x14, 0x5e, 0x41, 0x20, 0xd1, 0x53, + 0x1c, 0xb5, 0x35, 0x23, 0xa8, 0xba, 0x38, 0x70, + 0xa9, 0x0b, 0x3d, 0x67, 0xf1, 0xa1, 0x10, 0x97, + 0x21, 0x0b, 0xe4, 0x21, 0x02, 0x2e, 0x0d, 0xb7, + 0xd3, 0x14, 0x18, 0x09, 0xef, 0xb0, 0x45, 0x86, + 0xb4, 0x3b, 0x0b, 0x50, 0x6d, 0xe9, 0x78, 0xf8, + 0xfe, 0x09, 0x8c, 0x0c, 0xf8, 0x71, 0x67, 0x50 + }; + + /* Test exchange hash H. */ + unsigned char xcghash[] = { + 0xa4, 0xeb, 0xd4, 0x59, 0x34, 0xf5, 0x67, 0x92, + 0xb5, 0x11, 0x2d, 0xcd, 0x75, 0xa1, 0x07, 0x5f, + 0xdc, 0x88, 0x92, 0x45, 0x87, 0x12, 0x67, 0xe7, + 0xf6, 0x59, 0xf6, 0x8e, 0x5b, 0x22, 0x78, 0x22 + }; + + /* Test session ID. */ + unsigned char sessionId[] = { + 0xa4, 0xeb, 0xd4, 0x59, 0x34, 0xf5, 0x67, 0x92, + 0xb5, 0x11, 0x2d, 0xcd, 0x75, 0xa1, 0x07, 0x5f, + 0xdc, 0x88, 0x92, 0x45, 0x87, 0x12, 0x67, 0xe7, + 0xf6, 0x59, 0xf6, 0x8e, 0x5b, 0x22, 0x78, 0x22 + }; + + /* Test all key types 'A' through 'F'. */ + const char* types[] = { "A", "B", "C", "D", "E", "F" }; + + for (i = 0; i < 6; i++) { + PRINT_MSG("Testing SSHKDF with SHA-256 type '%s'", types[i]); + + /* Derive with OpenSSL. */ + err = test_sshkdf_calc(osslLibCtx, oKey, 32, "SHA-256", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), types[i]); + if (err != 0) { + PRINT_MSG("FAILED OpenSSL SSHKDF type '%s'", types[i]); + return err; + } + + /* Derive with wolfProvider. */ + err = test_sshkdf_calc(wpLibCtx, wKey, 32, "SHA-256", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), types[i]); + if (err != 0) { + PRINT_MSG("FAILED wolfProvider SSHKDF type '%s'", types[i]); + return err; + } + + if (memcmp(oKey, wKey, 32) != 0) { + PRINT_MSG("FAILED, wolfProvider and OpenSSL derived different keys for type '%s'", + types[i]); + PRINT_BUFFER("OpenSSL key", oKey, 32); + PRINT_BUFFER("wolfProvider key", wKey, 32); + return 1; + } + PRINT_MSG("PASSED SSHKDF type '%s'", types[i]); + } + + /* Test with a longer key derivation (> digest size). */ + PRINT_MSG("Testing SSHKDF with SHA-256 type 'A' - 64 byte output"); + err = test_sshkdf_calc(osslLibCtx, oKey, 64, "SHA-256", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED OpenSSL SSHKDF 64-byte"); + return err; + } + err = test_sshkdf_calc(wpLibCtx, wKey, 64, "SHA-256", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED wolfProvider SSHKDF 64-byte"); + return err; + } + if (memcmp(oKey, wKey, 64) != 0) { + PRINT_MSG("FAILED, wolfProvider and OpenSSL derived different 64-byte keys"); + PRINT_BUFFER("OpenSSL key", oKey, 64); + PRINT_BUFFER("wolfProvider key", wKey, 64); + return 1; + } + PRINT_MSG("PASSED SSHKDF 64-byte output"); + +#ifndef NO_SHA + /* Test with SHA-1. */ + PRINT_MSG("Testing SSHKDF with SHA-1 type 'A'"); + err = test_sshkdf_calc(osslLibCtx, oKey, 20, "SHA-1", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED OpenSSL SSHKDF SHA-1"); + return err; + } + err = test_sshkdf_calc(wpLibCtx, wKey, 20, "SHA-1", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED wolfProvider SSHKDF SHA-1"); + return err; + } + if (memcmp(oKey, wKey, 20) != 0) { + PRINT_MSG("FAILED, wolfProvider and OpenSSL derived different SHA-1 keys"); + PRINT_BUFFER("OpenSSL key", oKey, 20); + PRINT_BUFFER("wolfProvider key", wKey, 20); + return 1; + } + PRINT_MSG("PASSED SSHKDF SHA-1"); +#endif + +#ifdef WP_HAVE_SHA384 + /* Test with SHA-384. */ + PRINT_MSG("Testing SSHKDF with SHA-384 type 'A'"); + err = test_sshkdf_calc(osslLibCtx, oKey, 48, "SHA-384", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED OpenSSL SSHKDF SHA-384"); + return err; + } + err = test_sshkdf_calc(wpLibCtx, wKey, 48, "SHA-384", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED wolfProvider SSHKDF SHA-384"); + return err; + } + if (memcmp(oKey, wKey, 48) != 0) { + PRINT_MSG("FAILED, wolfProvider and OpenSSL derived different SHA-384 keys"); + PRINT_BUFFER("OpenSSL key", oKey, 48); + PRINT_BUFFER("wolfProvider key", wKey, 48); + return 1; + } + PRINT_MSG("PASSED SSHKDF SHA-384"); +#endif + +#ifdef WP_HAVE_SHA512 + /* Test with SHA-512. */ + PRINT_MSG("Testing SSHKDF with SHA-512 type 'A'"); + err = test_sshkdf_calc(osslLibCtx, oKey, 64, "SHA-512", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED OpenSSL SSHKDF SHA-512"); + return err; + } + err = test_sshkdf_calc(wpLibCtx, wKey, 64, "SHA-512", + inKey, sizeof(inKey), xcghash, sizeof(xcghash), + sessionId, sizeof(sessionId), "A"); + if (err != 0) { + PRINT_MSG("FAILED wolfProvider SSHKDF SHA-512"); + return err; + } + if (memcmp(oKey, wKey, 64) != 0) { + PRINT_MSG("FAILED, wolfProvider and OpenSSL derived different SHA-512 keys"); + PRINT_BUFFER("OpenSSL key", oKey, 64); + PRINT_BUFFER("wolfProvider key", wKey, 64); + return 1; + } + PRINT_MSG("PASSED SSHKDF SHA-512"); +#endif + + return err; +} + +/* Test error cases. */ +static int test_sshkdf_error_cases(OSSL_LIB_CTX* libCtx) +{ + int err; + unsigned char key[32]; + unsigned char inKey[] = { + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 + }; + unsigned char xcghash[] = { + 0xa4, 0xeb, 0xd4, 0x59, 0x34, 0xf5, 0x67, 0x92, + 0xb5, 0x11, 0x2d, 0xcd, 0x75, 0xa1, 0x07, 0x5f, + 0xdc, 0x88, 0x92, 0x45, 0x87, 0x12, 0x67, 0xe7, + 0xf6, 0x59, 0xf6, 0x8e, 0x5b, 0x22, 0x78, 0x22 + }; + unsigned char sessionId[] = { + 0xa4, 0xeb, 0xd4, 0x59, 0x34, 0xf5, 0x67, 0x92, + 0xb5, 0x11, 0x2d, 0xcd, 0x75, 0xa1, 0x07, 0x5f, + 0xdc, 0x88, 0x92, 0x45, 0x87, 0x12, 0x67, 0xe7, + 0xf6, 0x59, 0xf6, 0x8e, 0x5b, 0x22, 0x78, 0x22 + }; + + /* Test missing type - pass all params except type via a custom call. */ + PRINT_MSG("Testing SSHKDF error case - missing type"); + { + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[5], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("FAILED: Could not fetch SSHKDF"); + return 1; + } + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: Could not create SSHKDF context"); + return 1; + } + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA-256", 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, + inKey, sizeof(inKey)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, + xcghash, sizeof(xcghash)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, + sessionId, sizeof(sessionId)); + *p = OSSL_PARAM_construct_end(); + + err = EVP_KDF_derive(kctx, key, sizeof(key), params); + if (err > 0) { + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: SSHKDF should have failed with missing type"); + return 1; + } + PRINT_MSG("Negative test passed - SSHKDF correctly rejected missing type"); + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + } + + /* Test missing key. */ + PRINT_MSG("Testing SSHKDF error case - missing key"); + { + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[5], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("FAILED: Could not fetch SSHKDF"); + return 1; + } + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: Could not create SSHKDF context"); + return 1; + } + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA-256", 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, + xcghash, sizeof(xcghash)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, + sessionId, sizeof(sessionId)); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)"A", 1); + *p = OSSL_PARAM_construct_end(); + + err = EVP_KDF_derive(kctx, key, sizeof(key), params); + if (err > 0) { + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: SSHKDF should have failed with missing key"); + return 1; + } + PRINT_MSG("Negative test passed - SSHKDF correctly rejected missing key"); + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + } + + /* Test missing xcghash. */ + PRINT_MSG("Testing SSHKDF error case - missing xcghash"); + { + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[5], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("FAILED: Could not fetch SSHKDF"); + return 1; + } + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: Could not create SSHKDF context"); + return 1; + } + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA-256", 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, + inKey, sizeof(inKey)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, + sessionId, sizeof(sessionId)); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)"A", 1); + *p = OSSL_PARAM_construct_end(); + + err = EVP_KDF_derive(kctx, key, sizeof(key), params); + if (err > 0) { + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: SSHKDF should have failed with missing xcghash"); + return 1; + } + PRINT_MSG("Negative test passed - SSHKDF correctly rejected missing xcghash"); + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + } + + /* Test missing session ID. */ + PRINT_MSG("Testing SSHKDF error case - missing session ID"); + { + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[5], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("FAILED: Could not fetch SSHKDF"); + return 1; + } + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: Could not create SSHKDF context"); + return 1; + } + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA-256", 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, + inKey, sizeof(inKey)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, + xcghash, sizeof(xcghash)); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)"A", 1); + *p = OSSL_PARAM_construct_end(); + + err = EVP_KDF_derive(kctx, key, sizeof(key), params); + if (err > 0) { + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: SSHKDF should have failed with missing session ID"); + return 1; + } + PRINT_MSG("Negative test passed - SSHKDF correctly rejected missing session ID"); + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + } + + /* Test invalid type character. */ + PRINT_MSG("Testing SSHKDF error case - invalid type 'G'"); + { + EVP_KDF *kdf = NULL; + EVP_KDF_CTX *kctx = NULL; + OSSL_PARAM params[6], *p = params; + + kdf = EVP_KDF_fetch(libCtx, "SSHKDF", NULL); + if (kdf == NULL) { + PRINT_MSG("FAILED: Could not fetch SSHKDF"); + return 1; + } + kctx = EVP_KDF_CTX_new(kdf); + if (kctx == NULL) { + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: Could not create SSHKDF context"); + return 1; + } + + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST, + (char*)"SHA-256", 0); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, + inKey, sizeof(inKey)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_XCGHASH, + xcghash, sizeof(xcghash)); + *p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SSHKDF_SESSION_ID, + sessionId, sizeof(sessionId)); + *p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_SSHKDF_TYPE, + (char*)"G", 1); + *p = OSSL_PARAM_construct_end(); + + err = EVP_KDF_derive(kctx, key, sizeof(key), params); + if (err > 0) { + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + PRINT_MSG("FAILED: SSHKDF should have failed with invalid type 'G'"); + return 1; + } + PRINT_MSG("Negative test passed - SSHKDF correctly rejected invalid type 'G'"); + + EVP_KDF_CTX_free(kctx); + EVP_KDF_free(kdf); + } + + return 0; +} + +int test_sshkdf(void *data) +{ + int err = 0; + + (void)data; + + err = test_sshkdf_vector(); + if (err != 0) { + return err; + } + + /* Test error cases with OpenSSL first. */ + err = test_sshkdf_error_cases(osslLibCtx); + if (err != 0) { + return err; + } + + /* Test error cases with wolfProvider. */ + err = test_sshkdf_error_cases(wpLibCtx); + if (err != 0) { + return err; + } + + return err; +} + +#endif /* WP_HAVE_SSHKDF */ diff --git a/test/unit.c b/test/unit.c index 0e03de10..1f9276a7 100644 --- a/test/unit.c +++ b/test/unit.c @@ -223,6 +223,9 @@ TEST_CASE test_case[] = { #ifdef WP_HAVE_KRB5KDF TEST_DECL(test_krb5kdf, NULL), #endif +#ifdef WP_HAVE_SSHKDF + TEST_DECL(test_sshkdf, NULL), +#endif #ifdef WP_HAVE_DES3CBC #if !defined(HAVE_FIPS) || defined(WP_ALLOW_NON_FIPS) TEST_DECL(test_des3_cbc, NULL), diff --git a/test/unit.h b/test/unit.h index 6c87f91d..a2f90cc5 100644 --- a/test/unit.h +++ b/test/unit.h @@ -143,6 +143,9 @@ int test_kbkdf(void *data); #ifdef WP_HAVE_KRB5KDF int test_krb5kdf(void *data); #endif +#ifdef WP_HAVE_SSHKDF +int test_sshkdf(void *data); +#endif #ifdef WP_HAVE_DES3CBC int test_des3_cbc(void *data); From 049977d2bafd2345c3d43d094561f955a0e12f1e Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Thu, 2 Apr 2026 10:45:48 -0700 Subject: [PATCH 2/2] Add private key lock/unlock for FIPS --- src/wp_sshkdf.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp_sshkdf.c b/src/wp_sshkdf.c index 8c2fca94..cc12ecbd 100644 --- a/src/wp_sshkdf.c +++ b/src/wp_sshkdf.c @@ -364,11 +364,13 @@ static int wp_kdf_sshkdf_derive(wp_SshkdfCtx* ctx, unsigned char* key, } } + PRIVATE_KEY_UNLOCK(); rc = wc_SSH_KDF((byte)ctx->mdType, (byte)ctx->type, key, (word32)keyLen, rawKey, rawKeySz, ctx->xcghash, (word32)ctx->xcghashSz, ctx->sessionId, (word32)ctx->sessionIdSz); + PRIVATE_KEY_LOCK(); if (rc != 0) { WOLFPROV_MSG_DEBUG_RETCODE(WP_LOG_COMP_SSHKDF, "wc_SSH_KDF", rc); ok = 0;