From af8affc74093759063535461c4863a0b63a05e00 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 16:43:48 +0000 Subject: [PATCH 01/10] Support for importing external PSKs (RFC 9258) Port the external PSK importer interface from RFC 9258 to current master, gated behind WOLFSSL_EXTERNAL_PSK_IMPORTER. Based on commit 3d5c9e3 of the Laboratory-for-Safe-and-Secure-Systems wolfssl fork. * New client/server PSK importer callback interface, in addition to the existing TLS 1.3 PSK callbacks. * Support for the optional external PSK context. * Derive an ImportedIdentity (external_identity, context, target_protocol, target_kdf) from the external data and serialize it as the on-the-wire PSK identity. * Derive the internal (imported) PSK from the external one per RFC 9258 Section 3.1: epskx = HKDF-Extract(0, epsk) followed by ipskx = HKDF-Expand-Label(epskx, "derived psk", Hash(ImportedIdentity), L), with L set to the output length of the target_kdf. * Support both normal and pre-extracted external PSKs (wolfSSL_external_psk_pre_extracted). * Update PSK binder key derivation to use the "imp binder" label for imported PSKs (RFC 9258 Section 5). * Parse a received ImportedIdentity on the server side. The imported-PSK derivation uses SHA-256 as the importer hash function, which is the RFC 9258 default when the EPSK has no associated hash. The HKDF operations use the existing functional wc_HKDF_*_ex API so devId / CryptoCb offload is supported. The fork's PKCS#11 key-by-id/label path was omitted as it depends on a streaming HKDF API not present in master. Add an --enable-psk-importer configure option (and CMake equivalent) and a memio-based handshake test covering the SHA-256 and SHA-384 target KDFs, with and without a context, and the pre-extracted case. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- CMakeLists.txt | 18 ++ cmake/options.h.in | 2 + configure.ac | 21 ++ src/internal.c | 4 + src/tls.c | 281 +++++++++++++++++++++++++- src/tls13.c | 491 +++++++++++++++++++++++++++++++++++++++++++-- tests/api.c | 151 ++++++++++++++ wolfssl/internal.h | 42 +++- wolfssl/ssl.h | 22 ++ 9 files changed, 1011 insertions(+), 21 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 772da1456d5..f08d722a289 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -402,6 +402,24 @@ if(WOLFSSL_CERT_WITH_EXTERN_PSK) endif() endif() +# External PSK Importer (RFC 9258) +add_option("WOLFSSL_EXTERNAL_PSK_IMPORTER" + "Enable importing external PSKs for TLS 1.3 per RFC 9258 (default: disabled)" + "no" "yes;no") + +if(WOLFSSL_EXTERNAL_PSK_IMPORTER) + if(NOT WOLFSSL_TLS13) + message(WARNING "TLS 1.3 is disabled - disabling psk-importer") + override_cache(WOLFSSL_EXTERNAL_PSK_IMPORTER "no") + elseif(NOT WOLFSSL_PSK) + message(WARNING "PSK is disabled - disabling psk-importer") + override_cache(WOLFSSL_EXTERNAL_PSK_IMPORTER "no") + else() + list(APPEND WOLFSSL_DEFINITIONS + "-DWOLFSSL_EXTERNAL_PSK_IMPORTER") + endif() +endif() + # Hello Retry Request Cookie add_option("WOLFSSL_HRR_COOKIE" "Enable the server to send Cookie Extension in HRR with state (default: disabled)" diff --git a/cmake/options.h.in b/cmake/options.h.in index dfdee642ca6..eb81710189d 100644 --- a/cmake/options.h.in +++ b/cmake/options.h.in @@ -315,6 +315,8 @@ extern "C" { #cmakedefine WOLFSSL_DTLS_CH_FRAG #undef WOLFSSL_CERT_WITH_EXTERN_PSK #cmakedefine WOLFSSL_CERT_WITH_EXTERN_PSK +#undef WOLFSSL_EXTERNAL_PSK_IMPORTER +#cmakedefine WOLFSSL_EXTERNAL_PSK_IMPORTER #undef WOLFSSL_EITHER_SIDE #cmakedefine WOLFSSL_EITHER_SIDE #undef WOLFSSL_ENCRYPTED_KEYS diff --git a/configure.ac b/configure.ac index c1d2dd089ef..f7240362fd9 100644 --- a/configure.ac +++ b/configure.ac @@ -5298,6 +5298,27 @@ then fi fi +# External PSK Importer (RFC 9258) +AC_ARG_ENABLE([psk-importer], + [AS_HELP_STRING([--enable-psk-importer],[Enable importing external PSKs for TLS 1.3 per RFC 9258 (default: disabled)])], + [ ENABLED_PSK_IMPORTER=$enableval ], + [ ENABLED_PSK_IMPORTER=no ] + ) +if test "$ENABLED_PSK_IMPORTER" = "yes" +then + if test "$ENABLED_TLS13" = "no" + then + AC_MSG_NOTICE([TLS 1.3 is disabled - disabling psk-importer]) + ENABLED_PSK_IMPORTER="no" + elif test "$ENABLED_PSK" = "no" + then + AC_MSG_NOTICE([PSK is disabled - disabling psk-importer]) + ENABLED_PSK_IMPORTER="no" + else + AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_EXTERNAL_PSK_IMPORTER" + fi +fi + # ERROR STRINGS AC_ARG_ENABLE([errorstrings], [AS_HELP_STRING([--enable-errorstrings],[Enable error strings table (default: enabled)])], diff --git a/src/internal.c b/src/internal.c index fed9d370dea..60aa16157a5 100644 --- a/src/internal.c +++ b/src/internal.c @@ -7234,7 +7234,11 @@ int SetSSL_CTX(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup) ssl->options.client_psk_cs_cb = ctx->client_psk_cs_cb; ssl->options.client_psk_tls13_cb = ctx->client_psk_tls13_cb; ssl->options.server_psk_tls13_cb = ctx->server_psk_tls13_cb; +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + ssl->options.client_psk_importer_cb = ctx->client_psk_importer_cb; + ssl->options.server_psk_importer_cb = ctx->server_psk_importer_cb; #endif +#endif /* WOLFSSL_TLS13 */ #endif /* NO_PSK */ #ifdef WOLFSSL_EARLY_DATA if (ssl->options.side == WOLFSSL_SERVER_END) diff --git a/src/tls.c b/src/tls.c index ace0f7f979a..f904a3ce4fb 100644 --- a/src/tls.c +++ b/src/tls.c @@ -12620,6 +12620,146 @@ int TLSX_PreSharedKey_Use(TLSX** extensions, const byte* identity, word16 len, return 0; } +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +/* Create an ImportedIdentity object based on RFC 9258. + * + * ssl The SSL/TLS object. + * id The external identity. + * id_len Length of the external identity. + * ctx The optional external context. + * ctx_len Length of the external context. + * hmac The underlying HMAC algorithm to be used for the TLS HKDF. + * protocol The protocol version to be used for the connection. + * output The buffer to write the ImportedIdentity object into. + * out_len On entry, size of the output buffer. + * On exit, the number of bytes written into the buffer. + * + * When output is NULL and out_len is not NULL, the function will store the + * size of the buffer needed to write the ImportedIdentity object. + * Returns 0 on success and other values indicate failure. + */ +int TLSX_PreSharedKey_CreateImportedIdentity(const byte* id, word16 id_len, + const byte* ctx, word16 ctx_len, byte hmac, ProtocolVersion protocol, + byte* output, word16* out_len) +{ + int ret = 0; + word16 idx = 0; + + if ((output == NULL && out_len == NULL) || id == NULL || id_len == 0 || + !IsAtLeastTLSv1_3(protocol)) + return BAD_FUNC_ARG; + + if (output == NULL && out_len != NULL) { + /* Calculate the size of the ImportedIdentity object. Identity + * and context are both prefixed with their length as 16-bit. + * 'target_protocol' and 'target_kdf' are both 16-bit integers. + */ + *out_len = OPAQUE16_LEN + id_len + OPAQUE16_LEN + ctx_len + + OPAQUE16_LEN + OPAQUE16_LEN; + return 0; + } + + /* Check if output buffer is big enough */ + if (*out_len < (OPAQUE16_LEN + id_len + OPAQUE16_LEN + ctx_len + + OPAQUE16_LEN + OPAQUE16_LEN)) + return BUFFER_E; + + /* Write identity */ + c16toa(id_len, output + idx); + idx += OPAQUE16_LEN; + XMEMCPY(output + idx, id, id_len); + idx += id_len; + + /* Write context */ + if (ctx != NULL && ctx_len > 0) { + c16toa(ctx_len, output + idx); + idx += OPAQUE16_LEN; + XMEMCPY(output + idx, ctx, ctx_len); + idx += ctx_len; + } else { + c16toa(0, output + idx); + idx += OPAQUE16_LEN; + } + + /* Write target_protocol */ + output[idx++] = protocol.major; + output[idx++] = protocol.minor; + + /* Write target_kdf */ + if (hmac == sha256_mac) { + output[idx++] = 0x00; + output[idx++] = 0x01; + } else if (hmac == sha384_mac) { + output[idx++] = 0x00; + output[idx++] = 0x02; + } else + return BAD_FUNC_ARG; + + *out_len = idx; + + return ret; +} + +int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, word16 length, + byte** id, word16* id_len, byte** ctx, word16* ctx_len, + byte* hkdf, ProtocolVersion* protocol) +{ + int ret = 0; + word16 idx = 0; + word16 target_kdf = 0; + + /* Validate input. The minimum length is 4 times a 16-bit integer for + * the two length fields (id and ctx), the hkdf and the protocol version. + */ + if (input == NULL || length < (4 * OPAQUE16_LEN)) + return BAD_FUNC_ARG; + + /* Get identity len */ + ato16(input + idx, id_len); + idx += OPAQUE16_LEN; + if (*id_len > length - idx - (3 * OPAQUE16_LEN)) + return BUFFER_E; + + /* Get identity */ + if (*id_len > 0) { + *id = input + idx; + idx += *id_len; + } + else + *id = NULL; + + /* Get context len */ + ato16(input + idx, ctx_len); + idx += OPAQUE16_LEN; + if (*ctx_len > length - idx - (2 * OPAQUE16_LEN)) + return BUFFER_E; + + /* Get context */ + if (*ctx_len > 0) { + *ctx = input + idx; + idx += *ctx_len; + } + else + *ctx = NULL; + + /* Get target_protocol */ + protocol->major = input[idx++]; + protocol->minor = input[idx++]; + + /* Get target_kdf */ + ato16(input + idx, &target_kdf); + idx += OPAQUE16_LEN; + if (target_kdf == 0x0001) + *hkdf = sha256_mac; + else if (target_kdf == 0x0002) + *hkdf = sha384_mac; + else + return BAD_FUNC_ARG; + + return ret; +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ + #define PSK_FREE_ALL TLSX_PreSharedKey_FreeAll #define PSK_GET_SIZE TLSX_PreSharedKey_GetSize #define PSK_WRITE TLSX_PreSharedKey_Write @@ -16487,6 +16627,145 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) #endif #ifndef NO_PSK #ifndef WOLFSSL_PSK_ONE_ID + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (ssl->options.client_psk_importer_cb != NULL) { + int i; + word32 identitySz = 0; + word32 ctxSz = MAX_PSK_CTX_LEN; + ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; + #ifndef WOLFSSL_SMALL_STACK + byte ctx[MAX_PSK_CTX_LEN]; + #else + byte* ctx = (byte*)XMALLOC(MAX_PSK_CTX_LEN, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (ctx == NULL) + return MEMORY_ERROR; + #endif + + /* Get the external PSK data (identity, optional context and + * the actual key) by executing the user callback. */ + ret = ssl->options.client_psk_importer_cb(ssl, + ssl->arrays->client_identity, MAX_PSK_ID_LEN, ctx, + &ctxSz, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); + if (ret != 0) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + return PSK_KEY_ERROR; + } + + identitySz = XSTRLEN(ssl->arrays->client_identity); + if (identitySz > MAX_PSK_ID_LEN || + ctxSz > MAX_PSK_CTX_LEN || + ssl->arrays->psk_keySz > MAX_PSK_KEY_LEN) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + return PSK_KEY_ERROR; + } + + if (ssl->arrays->psk_keySz > 0 && identitySz > 0) { + const Suites* suites = WOLFSSL_SUITES(ssl); + + for (i = 0; i < suites->suiteSz; i += 2) { + byte* importedIdentity = NULL; + word16 importedIdentitySz = 0; + byte cipherSuite0 = suites->suites[i + 0]; + byte cipherSuite = suites->suites[i + 1]; + PreSharedKey* psk = NULL; + + #ifdef HAVE_NULL_CIPHER + if (cipherSuite0 == ECC_BYTE || + cipherSuite0 == ECDHE_PSK_BYTE) { + if (cipherSuite != TLS_SHA256_SHA256 && + cipherSuite != TLS_SHA384_SHA384) { + continue; + } + } + else + #endif + #if (defined(WOLFSSL_SM4_GCM) || \ + defined(WOLFSSL_SM4_CCM)) && \ + defined(WOLFSSL_SM3) + if (cipherSuite0 == CIPHER_BYTE) { + if ((cipherSuite != TLS_SM4_GCM_SM3) && + (cipherSuite != TLS_SM4_CCM_SM3)) { + continue; + } + } + else + #endif + if (cipherSuite0 != TLS13_BYTE) + continue; + + /* Get length of imported identity */ + ret = TLSX_PreSharedKey_CreateImportedIdentity( + (byte*)ssl->arrays->client_identity, identitySz, + ctx, ctxSz, + SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), + ssl->version, NULL, &importedIdentitySz); + if (ret != 0) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + return ret; + } + + importedIdentity = (byte*)XMALLOC(importedIdentitySz, + ssl->heap, + DYNAMIC_TYPE_TLSX); + if (importedIdentity == NULL) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + return MEMORY_ERROR; + } + + /* Generate the actual imported identity */ + ret = TLSX_PreSharedKey_CreateImportedIdentity( + (byte*)ssl->arrays->client_identity, identitySz, + ctx, ctxSz, + SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), + ssl->version, importedIdentity, + &importedIdentitySz); + if (ret != 0) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + XFREE(importedIdentity, ssl->heap, + DYNAMIC_TYPE_TLSX); + return ret; + } + + /* Store the PSK */ + ret = TLSX_PreSharedKey_Use(&ssl->extensions, + importedIdentity, importedIdentitySz, + 0, SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), + cipherSuite0, cipherSuite, 0, &psk, ssl->heap); + if (ret != 0) { + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + XFREE(importedIdentity, ssl->heap, + DYNAMIC_TYPE_TLSX); + return ret; + } + + /* Set flag in the PSK object to indicate that this + * one is imported. We use that flag to select the + * proper derive method for the binder key. */ + psk->imported = 1; + + XFREE(importedIdentity, ssl->heap, DYNAMIC_TYPE_TLSX); + usingPSK = 1; + } + } + #ifdef WOLFSSL_SMALL_STACK + XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + #endif + } + else + #endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ if (ssl->options.client_psk_cs_cb != NULL) { int i; const Suites* suites = WOLFSSL_SUITES(ssl); @@ -16554,7 +16833,7 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) usingPSK = 1; } else - #endif + #endif /* WOLFSSL_PSK_ONE_ID */ if (ssl->options.client_psk_cb != NULL || ssl->options.client_psk_tls13_cb != NULL) { /* Default cipher suite. */ diff --git a/src/tls13.c b/src/tls13.c index ecfd5946dd8..2944cde8c84 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -563,6 +563,31 @@ static int DeriveBinderKey(WOLFSSL* ssl, byte* key) binderKeyLabel, BINDER_KEY_LABEL_SZ, NULL, 0, ssl->specs.mac_algorithm); } + +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +/* The length of the binder key label. */ +#define BINDER_KEY_IMPORTED_LABEL_SZ 10 +/* The binder key imported label. */ +static const byte binderKeyImportedLabel[BINDER_KEY_IMPORTED_LABEL_SZ + 1] = + "imp binder"; + +/* Derive the binder key for imported PSKs. + * + * ssl The SSL/TLS object. + * key The derived key. + * returns 0 on success, otherwise failure. + */ +static int DeriveBinderKeyImported(WOLFSSL* ssl, byte* key) +{ + WOLFSSL_MSG("Derive Binder Key- Imported"); + if (ssl == NULL || ssl->arrays == NULL) { + return BAD_FUNC_ARG; + } + return DeriveKeyMsg(ssl, key, -1, ssl->arrays->secret, + binderKeyImportedLabel, BINDER_KEY_IMPORTED_LABEL_SZ, + NULL, 0, ssl->specs.mac_algorithm); +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ #endif /* !NO_PSK */ #if defined(HAVE_SESSION_TICKET) && \ @@ -1177,6 +1202,192 @@ static int Tls13_HKDF_Extract(WOLFSSL *ssl, byte* prk, const byte* salt, return ret; } +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +/* The length of the derived psk label. */ +#define DERIVED_PSK_LABEL_SZ 11 +/* The derived psk label. */ +static const byte derivedPskLabel[DERIVED_PSK_LABEL_SZ + 1] = + "derived psk"; + +/* Derive the imported PSK key from the external one and the created + * ImportedIdentity. + * + * ssl The SSL/TLS object. + */ +static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) +{ + int ret; + const byte* protocol; + word32 protocolLen; + word32 outputLen; + word32 hashSz = 0; + byte hash[WC_SHA256_DIGEST_SIZE]; + byte prk[WC_SHA256_DIGEST_SIZE]; + byte okm[MAX_PSK_KEY_LEN]; + word32 idx = 0; +#ifdef WOLFSSL_SMALL_STACK + byte* hkdfLabel = NULL; + wc_Sha256* sha256 = NULL; +#else + byte hkdfLabel[MAX_TLS13_HKDF_LABEL_SZ]; + wc_Sha256 sha256[1]; +#endif + + WOLFSSL_MSG("Derive Imported Pre-shared Key"); + if (ssl == NULL || ssl->arrays == NULL) { + return BAD_FUNC_ARG; + } + + /* Currently, SHA-256 is used as the underlying hash function for the HKDF + * operations below, following RFC 9258. In the future, the callback + * interface could be expanded to let the user provide a different hash + * algorithm. */ + +#ifdef WOLFSSL_SMALL_STACK + sha256 = (wc_Sha256*)XMALLOC(sizeof(wc_Sha256), ssl->heap, + DYNAMIC_TYPE_HASH_TMP); + if (sha256 == NULL) { + return MEMORY_E; + } +#endif + + /* Create the hash of the ImportedIdentity */ + ret = wc_InitSha256_ex(sha256, ssl->heap, ssl->devId); + if (ret == 0) { + ret = wc_Sha256Update(sha256, psk->identity, psk->identityLen); + if (ret == 0) + ret = wc_Sha256Final(sha256, hash); + wc_Sha256Free(sha256); + } +#ifdef WOLFSSL_SMALL_STACK + XFREE(sha256, ssl->heap, DYNAMIC_TYPE_HASH_TMP); +#endif + if (ret != 0) + return ret; + + hashSz = WC_SHA256_DIGEST_SIZE; + + switch (ssl->version.minor) { + case TLSv1_3_MINOR: + protocol = tls13ProtocolLabel; + protocolLen = TLS13_PROTOCOL_LABEL_SZ; + break; + #ifdef WOLFSSL_DTLS13 + case DTLSv1_3_MINOR: + if (!ssl->options.dtls) + return VERSION_ERROR; + + protocol = dtls13ProtocolLabel; + protocolLen = DTLS13_PROTOCOL_LABEL_SZ; + break; + #endif /* WOLFSSL_DTLS13 */ + default: + return VERSION_ERROR; + } + + /* The output length of the of the HDKF-Expand-Label operation below must + * match the hash-function of the KDF used further on in the handshake. + * This is encoded in psk->hmac, as this contains the target_kdf of the + * ImportedIdentity. */ + ret = wc_HashGetDigestSize(mac2hash(psk->hmac)); + if (ret < 0) + return ret; + else { + outputLen = (word32)ret; + ret = 0; + } + if (outputLen > (word32)sizeof(okm)) + return BUFFER_E; + + /* Check HkdfLabel length: okmLen (2) + protocol|label len (1) + + * info len(1) + protocollen + labellen + infolen + */ + idx = 4 + protocolLen + DERIVED_PSK_LABEL_SZ + hashSz; + if (idx > MAX_TLS13_HKDF_LABEL_SZ) + return BUFFER_E; + +#ifdef WOLFSSL_SMALL_STACK + hkdfLabel = (byte*)XMALLOC(idx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + if (hkdfLabel == NULL) + ret = MEMORY_E; +#endif + idx = 0; + + /* Construct the label */ + if (ret == 0) { + /* Output length. */ + hkdfLabel[idx++] = (byte)(outputLen >> 8); + hkdfLabel[idx++] = (byte)outputLen; + /* Length of protocol | label. */ + hkdfLabel[idx++] = (byte)(protocolLen + DERIVED_PSK_LABEL_SZ); + /* Protocol */ + XMEMCPY(&hkdfLabel[idx], protocol, protocolLen); + idx += protocolLen; + /* Label */ + XMEMCPY(&hkdfLabel[idx], derivedPskLabel, DERIVED_PSK_LABEL_SZ); + idx += DERIVED_PSK_LABEL_SZ; + /* Length of hash */ + hkdfLabel[idx++] = (byte)hashSz; + /* Hash of messages */ + XMEMCPY(&hkdfLabel[idx], hash, hashSz); + idx += hashSz; + + #ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Add("DeriveImportedPreSharedKey hkdfLabel", hkdfLabel, idx); + #endif + + /* okm holds ipskx; prk holds epskx. Both are dedicated buffers so the + * input and output of the HKDF operations never alias psk_key. */ + PRIVATE_KEY_UNLOCK(); + if (ssl->arrays->psk_externalKeyPreExtracted) { + /* The external PSK is already a pseudorandom key (the result of an + * earlier HKDF-Extract), so derive ipskx directly with an + * HKDF-Expand-Label. */ + ret = wc_HKDF_Expand_ex(WC_SHA256, ssl->arrays->psk_key, + ssl->arrays->psk_keySz, hkdfLabel, idx, okm, outputLen, + ssl->heap, ssl->devId); + } + else { + /* epskx = HKDF-Extract(0, epsk) */ + #if !defined(HAVE_FIPS) || \ + (defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)) + ret = wc_HKDF_Extract_ex(WC_SHA256, NULL, 0, ssl->arrays->psk_key, + ssl->arrays->psk_keySz, prk, ssl->heap, ssl->devId); + #else + ret = wc_HKDF_Extract(WC_SHA256, NULL, 0, ssl->arrays->psk_key, + ssl->arrays->psk_keySz, prk); + #endif + if (ret == 0) { + /* ipskx = HKDF-Expand-Label(epskx, "derived psk", + * Hash(ImportedIdentity), L) */ + ret = wc_HKDF_Expand_ex(WC_SHA256, prk, WC_SHA256_DIGEST_SIZE, + hkdfLabel, idx, okm, outputLen, ssl->heap, ssl->devId); + } + ForceZero(prk, sizeof(prk)); + } + PRIVATE_KEY_LOCK(); + + if (ret == 0) { + XMEMCPY(ssl->arrays->psk_key, okm, outputLen); + ssl->arrays->psk_keySz = outputLen; + } + + ForceZero(okm, sizeof(okm)); + ForceZero(hkdfLabel, idx); + + #ifdef WOLFSSL_CHECK_MEM_ZERO + wc_MemZero_Check(hkdfLabel, idx); + #endif + } + +#ifdef WOLFSSL_SMALL_STACK + XFREE(hkdfLabel, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); +#endif + + return ret; +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ + /* Derive the early secret using HKDF Extract. * * ssl The SSL/TLS object. @@ -4222,8 +4433,26 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello) if (psk->identityLen > MAX_PSK_ID_LEN) return PSK_KEY_ERROR; XMEMSET(ssl->arrays->client_identity, 0, - sizeof(ssl->arrays->client_identity)); - XMEMCPY(ssl->arrays->client_identity, psk->identity, psk->identityLen); + sizeof(ssl->arrays->client_identity)); + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (psk->imported) { + word16 identitySz = 0; + /* The identity buffer in the PSK object contains the full + * ImportedIdentity. Hence, we have to extract the actual + * identity here. The length of the identity is stored in + * the first two bytes. This has also already been verified + * to be less than MAX_PSK_ID_LEN during creation of the + * ImportedIdentity. */ + ato16(psk->identity, &identitySz); + XMEMCPY(ssl->arrays->client_identity, psk->identity + OPAQUE16_LEN, + identitySz); + } + else + #endif + { + XMEMCPY(ssl->arrays->client_identity, psk->identity, + psk->identityLen); + } #ifdef WOLFSSL_DEBUG_TLS WOLFSSL_MSG("PSK cipher suite:"); @@ -4269,6 +4498,52 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello) } else #endif /* OPENSSL_EXTRA */ + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (ssl->options.client_psk_importer_cb != NULL) { + word32 identitySz = 0; + ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; + + /* Lookup key again for next identity. */ + ret = ssl->options.client_psk_importer_cb(ssl, + ssl->arrays->client_identity, MAX_PSK_ID_LEN, NULL, + NULL, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); + if (ret != 0) + return PSK_KEY_ERROR; + + identitySz = XSTRLEN(ssl->arrays->client_identity); + + if (identitySz > MAX_PSK_ID_LEN || + ssl->arrays->psk_keySz > MAX_PSK_KEY_LEN) + return PSK_KEY_ERROR; + + /* Use PSK cipher suite. */ + if (clientHello) { + /* Use PSK cipher suite. */ + ssl->options.cipherSuite0 = psk->cipherSuite0; + ssl->options.cipherSuite = psk->cipherSuite; + } + else { + byte pskCS[2]; + pskCS[0] = psk->cipherSuite0; + pskCS[1] = psk->cipherSuite; + + /* Ensure PSK and negotiated cipher suites have same hash. */ + if (SuiteMac(pskCS) != SuiteMac(suite)) { + WOLFSSL_ERROR_VERBOSE(PSK_KEY_ERROR); + return PSK_KEY_ERROR; + } + /* Negotiated cipher suite is to be used - update PSK. */ + psk->cipherSuite0 = suite[0]; + psk->cipherSuite = suite[1]; + } + + /* Derive the imported PSK */ + ret = DeriveImportedPreSharedKey(ssl, psk); + if (ret != 0) + return ret; + } + else + #endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ if (ssl->options.client_psk_cs_cb != NULL) { #ifdef WOLFSSL_PSK_MULTI_ID_PER_CS ssl->arrays->client_identity[0] = 0; @@ -4467,8 +4742,14 @@ static int WritePSKBinders(WOLFSSL* ssl, byte* output, word32 idx) ret = DeriveBinderKeyResume(ssl, binderKey); #endif #ifndef NO_PSK - if (!current->resumption) - ret = DeriveBinderKey(ssl, binderKey); + if (!current->resumption) { + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (current->imported) + ret = DeriveBinderKeyImported(ssl, binderKey); + else + #endif + ret = DeriveBinderKey(ssl, binderKey); + } #endif if (ret != 0) break; @@ -6264,21 +6545,68 @@ int FindPskSuite(const WOLFSSL* ssl, PreSharedKey* psk, byte* psk_key, *found = 0; (void)suite; - if (ssl->options.server_psk_tls13_cb != NULL) { - *psk_keySz = ssl->options.server_psk_tls13_cb((WOLFSSL*)ssl, - (char*)psk->identity, psk_key, MAX_PSK_KEY_LEN, &cipherName); - if (*psk_keySz != 0) { - int cipherSuiteFlags = WOLFSSL_CIPHER_SUITE_FLAG_NONE; - *found = (GetCipherSuiteFromName(cipherName, &cipherSuite0, - &cipherSuite, NULL, NULL, &cipherSuiteFlags) == 0); - (void)cipherSuiteFlags; - } +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (ssl->options.server_psk_importer_cb != NULL) { + byte* id = NULL; + byte* ctx = NULL; + word16 idSz = 0; + word16 ctxSz = 0; + ProtocolVersion targetProtocol; + byte targetKdf = 0; + + /* Decode the received ImportedIdentity. If this call fails, the + * received identity may also be not an imported one. */ + if (TLSX_PreSharedKey_ParseImportedIdentity(psk->identity, + psk->identityLen, &id, &idSz, &ctx, &ctxSz, + &targetKdf, &targetProtocol) == 0) { + + /* Check if the targetKdf matches the KDF of the ciphersuite */ + if (SuiteMac(suite) == targetKdf) { + id[idSz] = '\0'; + *psk_keySz = MAX_PSK_KEY_LEN; + + /* Look for an external PSK for that identity and context. */ + ret = ssl->options.server_psk_importer_cb((WOLFSSL*)ssl, + (const char*)id, ctx, ctxSz, psk_key, psk_keySz); + + if (ret == 0 && *psk_keySz > 0 && + *psk_keySz <= MAX_PSK_KEY_LEN) { + /* Not yet set on the server-side */ + psk->hmac = targetKdf; + + /* Derive the actual imported PSK */ + ret = DeriveImportedPreSharedKey((WOLFSSL*)ssl, psk); + if (ret == 0) { + *found = 1; + + cipherSuite0 = suite[0]; + cipherSuite = suite[1]; + + /* Set flag in the PSK object to indicate that this + * one is imported. We use that flag to select the + * proper derive method for the binder key. */ + psk->imported = 1; + } + } + } + } + } +#endif + if (*found == 0 && (ssl->options.server_psk_tls13_cb != NULL)) { + *psk_keySz = ssl->options.server_psk_tls13_cb((WOLFSSL*)ssl, + (char*)psk->identity, psk_key, MAX_PSK_KEY_LEN, &cipherName); + if (*psk_keySz != 0) { + int cipherSuiteFlags = WOLFSSL_CIPHER_SUITE_FLAG_NONE; + *found = (GetCipherSuiteFromName(cipherName, &cipherSuite0, + &cipherSuite, NULL, NULL, &cipherSuiteFlags) == 0); + (void)cipherSuiteFlags; + } } if (*found == 0 && (ssl->options.server_psk_cb != NULL)) { - *psk_keySz = ssl->options.server_psk_cb((WOLFSSL*)ssl, - (char*)psk->identity, psk_key, - MAX_PSK_KEY_LEN); - *found = (*psk_keySz != 0); + *psk_keySz = ssl->options.server_psk_cb((WOLFSSL*)ssl, + (char*)psk->identity, psk_key, + MAX_PSK_KEY_LEN); + *found = (*psk_keySz != 0); } if (*found) { if (*psk_keySz > MAX_PSK_KEY_LEN && @@ -6581,7 +6909,13 @@ static int DoPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 inputSz, goto cleanup; /* Derive the binder key to use with HMAC. */ - ret = DeriveBinderKey(ssl, binderKey); + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + if (current->imported) + ret = DeriveBinderKeyImported(ssl, binderKey); + else + #endif + ret = DeriveBinderKey(ssl, binderKey); + if (ret != 0) goto cleanup; } @@ -15307,6 +15641,104 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, ssl->options.useAnon, TRUE, TRUE, TRUE, TRUE, ssl->options.side); } +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +/* Set the PSK callback that return the external identity, an optional context + * and the actual PSK to be imported for a client based on RFC 9258. + * + * @param [in, out] ssl SSL/TLS context object. + * @param [in] cb Client PSK importer callback. + */ +void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_client_importer_callback cb) +{ + WOLFSSL_ENTER("wolfSSL_CTX_set_psk_client_importer_callback"); + if (ctx == NULL) + return; + + ctx->havePSK = 1; + ctx->client_psk_importer_cb = cb; +} + +/* Set the PSK callback that return the external identity, an optional context + * and the actual PSK to be imported for a client based on RFC 9258. + * + * @param [in, out] ssl SSL/TLS object. + * @param [in] cb Client PSK importer callback. + */ +void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, + wc_psk_client_importer_callback cb) +{ + int keySz = 0; + + WOLFSSL_ENTER("wolfSSL_set_psk_client_importer_callback"); + + if (ssl == NULL) + return; + + ssl->options.havePSK = 1; + ssl->options.client_psk_importer_cb = cb; + +#ifndef NO_CERTS + keySz = ssl->buffers.keySz; +#endif + + if (AllocateSuites(ssl) != 0) + return; + + InitSuites(ssl->suites, ssl->version, keySz, ssl->options.haveRSA, TRUE, + ssl->options.haveDH, ssl->options.haveECDSAsig, + ssl->options.haveECC, TRUE, ssl->options.haveStaticECC, + ssl->options.useAnon, TRUE, TRUE, TRUE, TRUE, ssl->options.side); +} + +/* Set the PSK callback that return the external identity, an optional context + * and the actual PSK to be imported for a server based on RFC 9258. + * + * @param [in, out] ssl SSL/TLS context object. + * @param [in] cb Server PSK importer callback. + */ +void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_server_importer_callback cb) +{ + WOLFSSL_ENTER("wolfSSL_CTX_set_psk_server_importer_callback"); + if (ctx == NULL) + return; + + ctx->havePSK = 1; + ctx->server_psk_importer_cb = cb; +} + +/* Set the PSK callback that return the external identity, an optional context + * and the actual PSK to be imported for a server based on RFC 9258. + * + * @param [in, out] ssl SSL/TLS object. + * @param [in] cb Server PSK importer callback. + */ +void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, + wc_psk_server_importer_callback cb) +{ + int keySz = 0; + + WOLFSSL_ENTER("wolfSSL_set_psk_server_importer_callback"); + if (ssl == NULL) + return; + + ssl->options.havePSK = 1; + ssl->options.server_psk_importer_cb = cb; + +#ifndef NO_CERTS + keySz = ssl->buffers.keySz; +#endif + + if (AllocateSuites(ssl) != 0) + return; + InitSuites(ssl->suites, ssl->version, keySz, ssl->options.haveRSA, TRUE, + ssl->options.haveDH, ssl->options.haveECDSAsig, + ssl->options.haveECC, TRUE, ssl->options.haveStaticECC, + ssl->options.useAnon, TRUE, TRUE, TRUE, TRUE, ssl->options.side); +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ + /* Get name of first supported cipher suite that uses the hash indicated. * * @param [in] ssl SSL/TLS object. @@ -15346,6 +15778,29 @@ const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash) } return name; } + +#if defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) +/* Mark whether the external PSK provided by the importer callback is already a + * pre-extracted pseudorandom key. When set, the imported-PSK derivation skips + * the HKDF-Extract step (RFC 9258, Section 3.1). + * + * ssl The SSL/TLS object. + * opt Non-zero to treat the external PSK as pre-extracted. + * returns BAD_FUNC_ARG when ssl is NULL and 0 on success. + */ +int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt) +{ + if (ssl == NULL) + return BAD_FUNC_ARG; + + if (opt) + ssl->arrays->psk_externalKeyPreExtracted = 1; + else + ssl->arrays->psk_externalKeyPreExtracted = 0; + + return 0; +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ #endif /* !NO_PSK */ diff --git a/tests/api.c b/tests/api.c index 32b90b9a079..af00d6dbebd 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30398,6 +30398,156 @@ static int test_prioritize_psk(void) } #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) && !defined(WOLFSSL_PSK_ONE_ID) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_AESGCM) && !defined(NO_SHA256) && defined(WOLFSSL_AES_128) +/* Shared external PSK (EPSK) and ImportedIdentity inputs used by both the + * client and server importer callbacks (RFC 9258). */ +static const char test_psk_importer_identity[] = "9258 Client_identity"; +static const unsigned char test_psk_importer_epsk[] = { + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b +}; +static const char test_psk_importer_context[] = "RFC 9258 test context"; +/* Toggle whether the EPSK carries an optional context. */ +static int test_psk_importer_use_context = 0; + +static int test_psk_importer_client_cb(WOLFSSL* ssl, char* id, word32 idMax, + unsigned char* ctx, word32* ctxSz, unsigned char* key, word32* keySz) +{ + (void)ssl; + + if (XSTRLEN(test_psk_importer_identity) + 1 > idMax) + return -1; + XSTRNCPY(id, test_psk_importer_identity, idMax); + + if (ctx != NULL && ctxSz != NULL) { + if (test_psk_importer_use_context) { + word32 ctxLen = (word32)XSTRLEN(test_psk_importer_context); + if (ctxLen > *ctxSz) + return -1; + XMEMCPY(ctx, test_psk_importer_context, ctxLen); + *ctxSz = ctxLen; + } + else { + *ctxSz = 0; + } + } + + if ((word32)sizeof(test_psk_importer_epsk) > *keySz) + return -1; + XMEMCPY(key, test_psk_importer_epsk, sizeof(test_psk_importer_epsk)); + *keySz = (word32)sizeof(test_psk_importer_epsk); + + return 0; +} + +static int test_psk_importer_server_cb(WOLFSSL* ssl, const char* id, + const unsigned char* ctx, word32 ctxSz, unsigned char* key, + word32* keySz) +{ + (void)ssl; + + /* Match the received external identity. */ + if (id == NULL || XSTRCMP(id, test_psk_importer_identity) != 0) + return -1; + + /* Match the optional context exactly as advertised by the client. */ + if (test_psk_importer_use_context) { + word32 ctxLen = (word32)XSTRLEN(test_psk_importer_context); + if (ctxSz != ctxLen || ctx == NULL || + XMEMCMP(ctx, test_psk_importer_context, ctxLen) != 0) + return -1; + } + else if (ctxSz != 0) { + return -1; + } + + if ((word32)sizeof(test_psk_importer_epsk) > *keySz) + return -1; + XMEMCPY(key, test_psk_importer_epsk, sizeof(test_psk_importer_epsk)); + *keySz = (word32)sizeof(test_psk_importer_epsk); + + return 0; +} + +/* Run a single TLS 1.3 handshake that authenticates with an imported external + * PSK (RFC 9258), restricted to the given cipher suite. */ +static int test_tls13_external_psk_importer_one(const char* cipher, + int expectedSuite, int useContext, int preExtracted) +{ + EXPECT_DECLS; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + + test_psk_importer_use_context = useContext; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfTLSv1_3_client_method, wolfTLSv1_3_server_method), 0); + + /* PSK authenticates the peers; no certificate is required. */ + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_NONE, NULL); + + wolfSSL_set_psk_client_importer_callback(ssl_c, + test_psk_importer_client_cb); + wolfSSL_set_psk_server_importer_callback(ssl_s, + test_psk_importer_server_cb); + + if (preExtracted) { + ExpectIntEQ(wolfSSL_external_psk_pre_extracted(ssl_c, 1), 0); + ExpectIntEQ(wolfSSL_external_psk_pre_extracted(ssl_s, 1), 0); + } + + /* Restrict both sides to the suite under test (importer callback setup + * rebuilds the suite list, so set the cipher list afterwards). */ + ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, cipher), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_set_cipher_list(ssl_s, cipher), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + + ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_c), expectedSuite); + ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_s), expectedSuite); + + wolfSSL_free(ssl_c); + wolfSSL_free(ssl_s); + wolfSSL_CTX_free(ctx_c); + wolfSSL_CTX_free(ctx_s); + + return EXPECT_RESULT(); +} + +static int test_tls13_external_psk_importer(void) +{ + EXPECT_DECLS; + + /* HKDF_SHA256 target_kdf, without and with an optional context. */ + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", + 0x1301, 0, 0), TEST_SUCCESS); + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", + 0x1301, 1, 0), TEST_SUCCESS); + /* Pre-extracted EPSK (HKDF-Extract is skipped during import). */ + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", + 0x1301, 1, 1), TEST_SUCCESS); +#if defined(WOLFSSL_SHA384) && defined(WOLFSSL_AES_256) + /* HKDF_SHA384 target_kdf exercises the L = 48 derived-PSK length. */ + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES256-GCM-SHA384", + 0x1302, 1, 0), TEST_SUCCESS); +#endif + + return EXPECT_RESULT(); +} +#else +static int test_tls13_external_psk_importer(void) +{ + return TEST_SKIPPED; +} +#endif + #if defined(WOLFSSL_TLS13) && defined(OPENSSL_EXTRA) && \ defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && defined(HAVE_AESGCM) && \ !defined(NO_SHA256) && defined(WOLFSSL_AES_128) && \ @@ -35529,6 +35679,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_ticket_and_psk_mixing), /* Can't memory test as client/server Asserts in thread. */ TEST_DECL(test_prioritize_psk), + TEST_DECL(test_tls13_external_psk_importer), /* Can't memory test as client/server hangs. */ TEST_DECL(test_wc_CryptoCb), diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 3cd37c739bd..21a1e997106 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -1279,6 +1279,21 @@ enum { #endif #endif /* NO_DH */ +#ifndef MAX_PSK_ID_LEN + /* max psk identity/hint supported */ + #if defined(WOLFSSL_TLS13) + /* OpenSSL has a 1472 byte session ticket */ + #define MAX_PSK_ID_LEN 1536 + #else + #define MAX_PSK_ID_LEN 128 + #endif +#endif + +#ifndef MAX_PSK_CTX_LEN + /* maximum psk importer context size */ + #define MAX_PSK_CTX_LEN 128 +#endif + #ifndef MAX_PSK_KEY_LEN #define MAX_PSK_KEY_LEN 64 #endif @@ -3767,8 +3782,8 @@ WOLFSSL_LOCAL int TLSX_CKS_Parse(WOLFSSL* ssl, byte* input, word16 length, TLSX** extensions); WOLFSSL_LOCAL int TLSX_CKS_Set(WOLFSSL* ssl, TLSX** extensions); #endif -#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) +#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK) enum PskDecryptReturn { PSK_DECRYPT_NONE = 0, PSK_DECRYPT_OK, @@ -3809,6 +3824,9 @@ typedef struct PreSharedKey { byte resumption:1; /* Resumption PSK */ byte chosen:1; /* Server's choice */ byte decryptRet:3; /* Ticket decrypt return */ +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) + byte imported:1; /* PSK is imported */ +#endif struct PreSharedKey* next; /* List pointer */ } PreSharedKey; @@ -3847,6 +3865,15 @@ WOLFSSL_LOCAL int TLSX_PskKeyModes_Parse_Modes(const byte* input, word16 length, #ifdef WOLFSSL_EARLY_DATA WOLFSSL_LOCAL int TLSX_EarlyData_Use(WOLFSSL* ssl, word32 max, int is_response); #endif + +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +WOLFSSL_LOCAL int TLSX_PreSharedKey_CreateImportedIdentity(const byte* id, + word16 id_len, const byte* ctx, word16 ctx_len, byte hmac, + ProtocolVersion protocol, byte* output, word16* out_len); +WOLFSSL_LOCAL int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, + word16 length, byte** id, word16* id_len, byte** ctx,word16* ctx_len, + byte* hkdf, ProtocolVersion* protocol); +#endif #endif /* HAVE_SESSION_TICKET || !NO_PSK */ @@ -4163,7 +4190,11 @@ struct WOLFSSL_CTX { wc_psk_client_cs_callback client_psk_cs_cb; /* client callback */ wc_psk_client_tls13_callback client_psk_tls13_cb; /* client callback */ wc_psk_server_tls13_callback server_psk_tls13_cb; /* server callback */ +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + wc_psk_client_importer_callback client_psk_importer_cb; + wc_psk_server_importer_callback server_psk_importer_cb; #endif +#endif /* WOLFSSL_TLS13 */ void* psk_ctx; char server_hint[MAX_PSK_ID_LEN + NULL_TERM_LEN]; #endif /* HAVE_SESSION_TICKET || !NO_PSK */ @@ -5104,7 +5135,11 @@ struct Options { wc_psk_client_cs_callback client_psk_cs_cb; /* client callback */ wc_psk_client_tls13_callback client_psk_tls13_cb; /* client callback */ wc_psk_server_tls13_callback server_psk_tls13_cb; /* server callback */ +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + wc_psk_client_importer_callback client_psk_importer_cb; + wc_psk_server_importer_callback server_psk_importer_cb; #endif +#endif /* WOLFSSL_TLS13 */ void* psk_ctx; #endif /* NO_PSK */ unsigned long mask; /* store SSL_OP_ flags */ @@ -5377,7 +5412,10 @@ typedef struct Arrays { char client_identity[MAX_PSK_ID_LEN + NULL_TERM_LEN]; char server_hint[MAX_PSK_ID_LEN + NULL_TERM_LEN]; byte psk_key[MAX_PSK_KEY_LEN]; -#endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) + byte psk_externalKeyPreExtracted:1; +#endif /* WOLFSSL_TLS13 && WOLFSSL_EXTERNAL_PSK_IMPORTER */ +#endif /* HAVE_SESSION_TICKET || !NO_PSK */ byte clientRandom[RAN_LEN]; #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) byte clientRandomInner[RAN_LEN]; diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 738828b127b..6103c3080c5 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -3280,6 +3280,15 @@ enum { /* ssl Constants */ wc_psk_client_tls13_callback cb); WOLFSSL_API void wolfSSL_set_psk_client_tls13_callback(WOLFSSL* ssl, wc_psk_client_tls13_callback cb); + + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl, char*, + word32, unsigned char*, word32*, unsigned char*, word32*); + WOLFSSL_API void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_client_importer_callback cb); + WOLFSSL_API void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, + wc_psk_client_importer_callback cb); + #endif #endif WOLFSSL_API const char* wolfSSL_get_psk_identity_hint(const WOLFSSL* ssl); @@ -3301,6 +3310,15 @@ enum { /* ssl Constants */ wc_psk_server_tls13_callback cb); WOLFSSL_API void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, wc_psk_server_tls13_callback cb); + + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl, const char*, + const unsigned char*, word32, unsigned char*, word32*); + WOLFSSL_API void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_server_importer_callback cb); + WOLFSSL_API void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, + wc_psk_server_importer_callback cb); + #endif #endif #if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_WITH_EXTERN_PSK) WOLFSSL_API int wolfSSL_CTX_set_cert_with_extern_psk(WOLFSSL_CTX* ctx, @@ -3318,6 +3336,10 @@ enum { /* ssl Constants */ #ifdef WOLFSSL_TLS13 WOLFSSL_API const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash); + + #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER + WOLFSSL_API int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt); + #endif #endif #endif /* NO_PSK */ From 92db1fd45242ee7dd1fb909f27ea91a7436df5d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 17:18:03 +0000 Subject: [PATCH 02/10] RFC 9258 importer: fix review findings Address code-review feedback on the external PSK importer: * Security: reject a zero-length external_identity when parsing a received ImportedIdentity. Previously such input left the id pointer NULL, which the server then dereferenced/wrote to (id[idSz] = '\0') -> remotely triggerable NULL pointer write. RFC 9258 defines external_identity<1..2^16-1>. * Server side no longer mutates the received message buffer: the identity is copied into a heap NUL-terminated string for the callback. * Server now validates target_protocol (in addition to target_kdf) against the negotiated version before treating an identity as imported. * Client side derives one ImportedIdentity per (protocol, KDF) and skips cipher suites whose MAC is not a defined RFC 9258 target KDF, avoiding duplicate PSK identities and a BAD_FUNC_ARG on non-SHA256/384 suites. Consolidated the per-iteration cleanup via a single goto path and fixed word32->word16 narrowing. * Register the derived-key buffers (okm/prk) with WOLFSSL_CHECK_MEM_ZERO. * Fixed the CreateImportedIdentity doc comment (no ssl parameter) and added a doc comment for ParseImportedIdentity. * Added Doxygen API documentation for the new public importer functions. * Tests: added a direct ImportedIdentity (de)serialization unit test covering empty-identity, truncated, overflow and bad-KDF inputs; added a negative binder-mismatch handshake; broadened the test guard so it also runs in PSK-only (no certificate) builds. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- doc/dox_comments/header_files/ssl.h | 129 ++++++++++++++++++++++++++++ src/tls.c | 129 ++++++++++++++++------------ src/tls13.c | 70 +++++++++------ tests/api.c | 112 +++++++++++++++++++++--- 4 files changed, 348 insertions(+), 92 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 1a56a4ae7c7..9df96eb974c 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15221,6 +15221,135 @@ void wolfSSL_CTX_set_psk_server_tls13_callback(WOLFSSL_CTX* ctx, void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, wc_psk_server_tls13_callback cb); +/*! + \ingroup Setup + + \brief This function sets, on the WOLFSSL_CTX, the client side callback used + to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per + RFC 9258. The callback returns the external identity, an optional context + and the external PSK; wolfSSL derives the ImportedIdentity and the imported + PSK used in the handshake. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + + \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. + \param [in] cb a client PSK importer callback. + + _Example_ + \code + WOLFSSL_CTX* ctx; + ... + wolfSSL_CTX_set_psk_client_importer_callback(ctx, my_psk_importer_cb); + \endcode + + \sa wolfSSL_set_psk_client_importer_callback + \sa wolfSSL_CTX_set_psk_server_importer_callback + \sa wolfSSL_external_psk_pre_extracted +*/ +void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_client_importer_callback cb); + +/*! + \ingroup Setup + + \brief This function sets, on the WOLFSSL object, the client side callback + used to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per + RFC 9258. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + + \param [in,out] ssl a pointer to a WOLFSSL structure, created using + wolfSSL_new(). + \param [in] cb a client PSK importer callback. + + _Example_ + \code + WOLFSSL* ssl; + ... + wolfSSL_set_psk_client_importer_callback(ssl, my_psk_importer_cb); + \endcode + + \sa wolfSSL_CTX_set_psk_client_importer_callback + \sa wolfSSL_set_psk_server_importer_callback + \sa wolfSSL_external_psk_pre_extracted +*/ +void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, + wc_psk_client_importer_callback cb); + +/*! + \ingroup Setup + + \brief This function sets, on the WOLFSSL_CTX, the server side callback used + to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per + RFC 9258. Given a received external identity and optional context, the + callback returns the matching external PSK. Requires + WOLFSSL_EXTERNAL_PSK_IMPORTER. + + \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. + \param [in] cb a server PSK importer callback. + + _Example_ + \code + WOLFSSL_CTX* ctx; + ... + wolfSSL_CTX_set_psk_server_importer_callback(ctx, my_psk_importer_cb); + \endcode + + \sa wolfSSL_set_psk_server_importer_callback + \sa wolfSSL_CTX_set_psk_client_importer_callback + \sa wolfSSL_external_psk_pre_extracted +*/ +void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, + wc_psk_server_importer_callback cb); + +/*! + \ingroup Setup + + \brief This function sets, on the WOLFSSL object, the server side callback + used to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per + RFC 9258. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + + \param [in,out] ssl a pointer to a WOLFSSL structure, created using + wolfSSL_new(). + \param [in] cb a server PSK importer callback. + + _Example_ + \code + WOLFSSL* ssl; + ... + wolfSSL_set_psk_server_importer_callback(ssl, my_psk_importer_cb); + \endcode + + \sa wolfSSL_CTX_set_psk_server_importer_callback + \sa wolfSSL_set_psk_client_importer_callback + \sa wolfSSL_external_psk_pre_extracted +*/ +void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, + wc_psk_server_importer_callback cb); + +/*! + \ingroup Setup + + \brief This function marks whether the external PSK supplied by the importer + callback is already a pre-extracted pseudorandom key. When enabled, the + imported-PSK derivation skips the HKDF-Extract step (RFC 9258, Section 3.1). + Both peers must agree on this setting. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + + \return 0 on success. + \return BAD_FUNC_ARG when ssl is NULL. + + \param [in,out] ssl a pointer to a WOLFSSL structure, created using + wolfSSL_new(). + \param [in] opt non-zero to treat the external PSK as pre-extracted. + + _Example_ + \code + WOLFSSL* ssl; + ... + wolfSSL_external_psk_pre_extracted(ssl, 1); + \endcode + + \sa wolfSSL_set_psk_client_importer_callback + \sa wolfSSL_set_psk_server_importer_callback +*/ +int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt); + /*! \ingroup Setup diff --git a/src/tls.c b/src/tls.c index f904a3ce4fb..27565209d7d 100644 --- a/src/tls.c +++ b/src/tls.c @@ -12623,7 +12623,6 @@ int TLSX_PreSharedKey_Use(TLSX** extensions, const byte* identity, word16 len, #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER /* Create an ImportedIdentity object based on RFC 9258. * - * ssl The SSL/TLS object. * id The external identity. * id_len Length of the external identity. * ctx The optional external context. @@ -12700,6 +12699,20 @@ int TLSX_PreSharedKey_CreateImportedIdentity(const byte* id, word16 id_len, return ret; } +/* Parse an ImportedIdentity object received on the wire (RFC 9258). + * + * input The serialized ImportedIdentity. The returned id/ctx pointers + * alias into this buffer; the caller must not modify it. + * length Length of the serialized ImportedIdentity. + * id On exit, points to the external identity within input. + * id_len On exit, length of the external identity (always non-zero). + * ctx On exit, points to the optional context within input, or NULL. + * ctx_len On exit, length of the optional context (may be zero). + * hkdf On exit, the HMAC algorithm matching target_kdf. + * protocol On exit, the parsed target_protocol. + * + * Returns 0 on success and other values indicate failure. + */ int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, word16 length, byte** id, word16* id_len, byte** ctx, word16* ctx_len, byte* hkdf, ProtocolVersion* protocol) @@ -12714,19 +12727,19 @@ int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, word16 length, if (input == NULL || length < (4 * OPAQUE16_LEN)) return BAD_FUNC_ARG; - /* Get identity len */ + /* Get identity len. RFC 9258 defines external_identity<1..2^16-1>, so an + * empty identity is invalid and must be rejected (a zero-length identity + * would otherwise leave *id NULL for callers to dereference). */ ato16(input + idx, id_len); idx += OPAQUE16_LEN; + if (*id_len == 0) + return BAD_FUNC_ARG; if (*id_len > length - idx - (3 * OPAQUE16_LEN)) return BUFFER_E; /* Get identity */ - if (*id_len > 0) { - *id = input + idx; - idx += *id_len; - } - else - *id = NULL; + *id = input + idx; + idx += *id_len; /* Get context len */ ato16(input + idx, ctx_len); @@ -16632,7 +16645,12 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) int i; word32 identitySz = 0; word32 ctxSz = MAX_PSK_CTX_LEN; - ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; + byte* importedIdentity = NULL; + /* Track which target KDFs already produced an identity so a + * single KDF is not emitted more than once (RFC 9258 derives + * one IPSK per (protocol, KDF), independent of cipher suite). + * Index 0 = HKDF_SHA256, index 1 = HKDF_SHA384. */ + byte seenKdf[2]; #ifndef WOLFSSL_SMALL_STACK byte ctx[MAX_PSK_CTX_LEN]; #else @@ -16641,6 +16659,8 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) if (ctx == NULL) return MEMORY_ERROR; #endif + ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; + XMEMSET(seenKdf, 0, sizeof(seenKdf)); /* Get the external PSK data (identity, optional context and * the actual key) by executing the user callback. */ @@ -16648,30 +16668,26 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) ssl->arrays->client_identity, MAX_PSK_ID_LEN, ctx, &ctxSz, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); if (ret != 0) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return PSK_KEY_ERROR; + ret = PSK_KEY_ERROR; + goto importer_cleanup; } identitySz = XSTRLEN(ssl->arrays->client_identity); if (identitySz > MAX_PSK_ID_LEN || ctxSz > MAX_PSK_CTX_LEN || ssl->arrays->psk_keySz > MAX_PSK_KEY_LEN) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return PSK_KEY_ERROR; + ret = PSK_KEY_ERROR; + goto importer_cleanup; } if (ssl->arrays->psk_keySz > 0 && identitySz > 0) { const Suites* suites = WOLFSSL_SUITES(ssl); for (i = 0; i < suites->suiteSz; i += 2) { - byte* importedIdentity = NULL; word16 importedIdentitySz = 0; byte cipherSuite0 = suites->suites[i + 0]; byte cipherSuite = suites->suites[i + 1]; + byte suiteMac; PreSharedKey* psk = NULL; #ifdef HAVE_NULL_CIPHER @@ -16698,58 +16714,57 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) if (cipherSuite0 != TLS13_BYTE) continue; + /* RFC 9258 only defines the HKDF_SHA256 and + * HKDF_SHA384 target KDFs. Skip suites with any other + * MAC, and skip a KDF already handled. */ + suiteMac = SuiteMac(suites->suites + i); + if (suiteMac == sha256_mac) { + if (seenKdf[0]) + continue; + seenKdf[0] = 1; + } + else if (suiteMac == sha384_mac) { + if (seenKdf[1]) + continue; + seenKdf[1] = 1; + } + else { + continue; + } + /* Get length of imported identity */ ret = TLSX_PreSharedKey_CreateImportedIdentity( - (byte*)ssl->arrays->client_identity, identitySz, - ctx, ctxSz, - SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), - ssl->version, NULL, &importedIdentitySz); - if (ret != 0) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return ret; - } + (byte*)ssl->arrays->client_identity, + (word16)identitySz, ctx, (word16)ctxSz, + suiteMac, ssl->version, NULL, + &importedIdentitySz); + if (ret != 0) + goto importer_cleanup; importedIdentity = (byte*)XMALLOC(importedIdentitySz, ssl->heap, DYNAMIC_TYPE_TLSX); if (importedIdentity == NULL) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - return MEMORY_ERROR; + ret = MEMORY_ERROR; + goto importer_cleanup; } /* Generate the actual imported identity */ ret = TLSX_PreSharedKey_CreateImportedIdentity( - (byte*)ssl->arrays->client_identity, identitySz, - ctx, ctxSz, - SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), - ssl->version, importedIdentity, + (byte*)ssl->arrays->client_identity, + (word16)identitySz, ctx, (word16)ctxSz, + suiteMac, ssl->version, importedIdentity, &importedIdentitySz); - if (ret != 0) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - XFREE(importedIdentity, ssl->heap, - DYNAMIC_TYPE_TLSX); - return ret; - } + if (ret != 0) + goto importer_cleanup; /* Store the PSK */ ret = TLSX_PreSharedKey_Use(&ssl->extensions, importedIdentity, importedIdentitySz, - 0, SuiteMac(WOLFSSL_SUITES(ssl)->suites + i), - cipherSuite0, cipherSuite, 0, &psk, ssl->heap); - if (ret != 0) { - #ifdef WOLFSSL_SMALL_STACK - XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - #endif - XFREE(importedIdentity, ssl->heap, - DYNAMIC_TYPE_TLSX); - return ret; - } + 0, suiteMac, cipherSuite0, cipherSuite, 0, &psk, + ssl->heap); + if (ret != 0) + goto importer_cleanup; /* Set flag in the PSK object to indicate that this * one is imported. We use that flag to select the @@ -16757,12 +16772,18 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) psk->imported = 1; XFREE(importedIdentity, ssl->heap, DYNAMIC_TYPE_TLSX); + importedIdentity = NULL; usingPSK = 1; } } + ret = 0; + importer_cleanup: + XFREE(importedIdentity, ssl->heap, DYNAMIC_TYPE_TLSX); #ifdef WOLFSSL_SMALL_STACK XFREE(ctx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); #endif + if (ret != 0) + return ret; } else #endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ diff --git a/src/tls13.c b/src/tls13.c index 2944cde8c84..cfbfae16e41 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -1334,6 +1334,8 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) #ifdef WOLFSSL_CHECK_MEM_ZERO wc_MemZero_Add("DeriveImportedPreSharedKey hkdfLabel", hkdfLabel, idx); + wc_MemZero_Add("DeriveImportedPreSharedKey okm", okm, sizeof(okm)); + wc_MemZero_Add("DeriveImportedPreSharedKey prk", prk, sizeof(prk)); #endif /* okm holds ipskx; prk holds epskx. Both are dedicated buffers so the @@ -1363,7 +1365,6 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) ret = wc_HKDF_Expand_ex(WC_SHA256, prk, WC_SHA256_DIGEST_SIZE, hkdfLabel, idx, okm, outputLen, ssl->heap, ssl->devId); } - ForceZero(prk, sizeof(prk)); } PRIVATE_KEY_LOCK(); @@ -1373,10 +1374,13 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) } ForceZero(okm, sizeof(okm)); + ForceZero(prk, sizeof(prk)); ForceZero(hkdfLabel, idx); #ifdef WOLFSSL_CHECK_MEM_ZERO wc_MemZero_Check(hkdfLabel, idx); + wc_MemZero_Check(prk, sizeof(prk)); + wc_MemZero_Check(okm, sizeof(okm)); #endif } @@ -6554,39 +6558,49 @@ int FindPskSuite(const WOLFSSL* ssl, PreSharedKey* psk, byte* psk_key, ProtocolVersion targetProtocol; byte targetKdf = 0; + XMEMSET(&targetProtocol, 0, sizeof(targetProtocol)); + /* Decode the received ImportedIdentity. If this call fails, the * received identity may also be not an imported one. */ if (TLSX_PreSharedKey_ParseImportedIdentity(psk->identity, psk->identityLen, &id, &idSz, &ctx, &ctxSz, - &targetKdf, &targetProtocol) == 0) { - - /* Check if the targetKdf matches the KDF of the ciphersuite */ - if (SuiteMac(suite) == targetKdf) { - id[idSz] = '\0'; - *psk_keySz = MAX_PSK_KEY_LEN; - - /* Look for an external PSK for that identity and context. */ - ret = ssl->options.server_psk_importer_cb((WOLFSSL*)ssl, - (const char*)id, ctx, ctxSz, psk_key, psk_keySz); - - if (ret == 0 && *psk_keySz > 0 && - *psk_keySz <= MAX_PSK_KEY_LEN) { - /* Not yet set on the server-side */ - psk->hmac = targetKdf; - - /* Derive the actual imported PSK */ - ret = DeriveImportedPreSharedKey((WOLFSSL*)ssl, psk); - if (ret == 0) { - *found = 1; + &targetKdf, &targetProtocol) == 0 && + /* The KDF and protocol bound into the ImportedIdentity must match + * the ones being negotiated (RFC 9258, Section 3.1). */ + SuiteMac(suite) == targetKdf && + targetProtocol.major == ssl->version.major && + targetProtocol.minor == ssl->version.minor) { + /* The importer callback expects a NUL-terminated identity. Copy + * it out instead of mutating the received message buffer. */ + char* idStr = (char*)XMALLOC((word32)idSz + 1, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + if (idStr == NULL) + return MEMORY_ERROR; + XMEMCPY(idStr, id, idSz); + idStr[idSz] = '\0'; + *psk_keySz = MAX_PSK_KEY_LEN; + + /* Look for an external PSK for that identity and context. */ + ret = ssl->options.server_psk_importer_cb((WOLFSSL*)ssl, + (const char*)idStr, ctx, ctxSz, psk_key, psk_keySz); + XFREE(idStr, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + + if (ret == 0 && *psk_keySz > 0 && *psk_keySz <= MAX_PSK_KEY_LEN) { + /* Not yet set on the server-side */ + psk->hmac = targetKdf; + + /* Derive the actual imported PSK */ + ret = DeriveImportedPreSharedKey((WOLFSSL*)ssl, psk); + if (ret == 0) { + *found = 1; - cipherSuite0 = suite[0]; - cipherSuite = suite[1]; + cipherSuite0 = suite[0]; + cipherSuite = suite[1]; - /* Set flag in the PSK object to indicate that this - * one is imported. We use that flag to select the - * proper derive method for the binder key. */ - psk->imported = 1; - } + /* Set flag in the PSK object to indicate that this + * one is imported. We use that flag to select the + * proper derive method for the binder key. */ + psk->imported = 1; } } } diff --git a/tests/api.c b/tests/api.c index af00d6dbebd..062803b4f0d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30400,7 +30400,7 @@ static int test_prioritize_psk(void) #if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ !defined(NO_PSK) && !defined(WOLFSSL_PSK_ONE_ID) && \ - defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES_BUILD) && \ defined(HAVE_AESGCM) && !defined(NO_SHA256) && defined(WOLFSSL_AES_128) /* Shared external PSK (EPSK) and ImportedIdentity inputs used by both the * client and server importer callbacks (RFC 9258). */ @@ -30412,6 +30412,8 @@ static const unsigned char test_psk_importer_epsk[] = { static const char test_psk_importer_context[] = "RFC 9258 test context"; /* Toggle whether the EPSK carries an optional context. */ static int test_psk_importer_use_context = 0; +/* When set, the server returns a different EPSK so the binder must not match. */ +static int test_psk_importer_server_wrong_key = 0; static int test_psk_importer_client_cb(WOLFSSL* ssl, char* id, word32 idMax, unsigned char* ctx, word32* ctxSz, unsigned char* key, word32* keySz) @@ -30467,6 +30469,10 @@ static int test_psk_importer_server_cb(WOLFSSL* ssl, const char* id, if ((word32)sizeof(test_psk_importer_epsk) > *keySz) return -1; XMEMCPY(key, test_psk_importer_epsk, sizeof(test_psk_importer_epsk)); + if (test_psk_importer_server_wrong_key) { + /* Flip a byte so the derived PSK (and thus the binder) differs. */ + key[0] ^= 0xFF; + } *keySz = (word32)sizeof(test_psk_importer_epsk); return 0; @@ -30475,7 +30481,7 @@ static int test_psk_importer_server_cb(WOLFSSL* ssl, const char* id, /* Run a single TLS 1.3 handshake that authenticates with an imported external * PSK (RFC 9258), restricted to the given cipher suite. */ static int test_tls13_external_psk_importer_one(const char* cipher, - int expectedSuite, int useContext, int preExtracted) + int expectedSuite, int useContext, int preExtracted, int expectFail) { EXPECT_DECLS; WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; @@ -30508,10 +30514,15 @@ static int test_tls13_external_psk_importer_one(const char* cipher, ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, cipher), WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_set_cipher_list(ssl_s, cipher), WOLFSSL_SUCCESS); - ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); - - ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_c), expectedSuite); - ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_s), expectedSuite); + if (expectFail) { + /* A mismatched imported PSK must abort the handshake. */ + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + } + else { + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_c), expectedSuite); + ExpectIntEQ(wolfSSL_get_current_cipher_suite(ssl_s), expectedSuite); + } wolfSSL_free(ssl_c); wolfSSL_free(ssl_s); @@ -30525,20 +30536,96 @@ static int test_tls13_external_psk_importer(void) { EXPECT_DECLS; + test_psk_importer_server_wrong_key = 0; + /* HKDF_SHA256 target_kdf, without and with an optional context. */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 0, 0), TEST_SUCCESS); + 0x1301, 0, 0, 0), TEST_SUCCESS); ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 0), TEST_SUCCESS); + 0x1301, 1, 0, 0), TEST_SUCCESS); /* Pre-extracted EPSK (HKDF-Extract is skipped during import). */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 1), TEST_SUCCESS); + 0x1301, 1, 1, 0), TEST_SUCCESS); #if defined(WOLFSSL_SHA384) && defined(WOLFSSL_AES_256) /* HKDF_SHA384 target_kdf exercises the L = 48 derived-PSK length. */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES256-GCM-SHA384", - 0x1302, 1, 0), TEST_SUCCESS); + 0x1302, 1, 0, 0), TEST_SUCCESS); #endif + /* Negative: server derives a different imported PSK -> binder mismatch + * -> handshake must fail. */ + test_psk_importer_server_wrong_key = 1; + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", + 0x1301, 1, 0, 1), TEST_SUCCESS); + test_psk_importer_server_wrong_key = 0; + + return EXPECT_RESULT(); +} + +/* Directly exercise ImportedIdentity (de)serialization, including the + * malformed inputs a peer could send (RFC 9258, Section 3.1). */ +static int test_tls13_external_psk_importer_parse(void) +{ + EXPECT_DECLS; + byte out[128]; + word16 outLen = (word16)sizeof(out); + const char* id = "abc"; + const char* ctx = "xy"; + ProtocolVersion pv; + byte* pId = NULL; + byte* pCtx = NULL; + word16 pIdLen = 0; + word16 pCtxLen = 0; + byte pHkdf = 0; + ProtocolVersion pProto; + + pv.major = SSLv3_MAJOR; + pv.minor = TLSv1_3_MINOR; + + /* Round-trip a well-formed ImportedIdentity. */ + ExpectIntEQ(TLSX_PreSharedKey_CreateImportedIdentity((const byte*)id, 3, + (const byte*)ctx, 2, sha256_mac, pv, out, &outLen), 0); + ExpectIntEQ(TLSX_PreSharedKey_ParseImportedIdentity(out, outLen, &pId, + &pIdLen, &pCtx, &pCtxLen, &pHkdf, &pProto), 0); + ExpectIntEQ(pIdLen, 3); + ExpectNotNull(pId); + ExpectIntEQ(XMEMCMP(pId, id, 3), 0); + ExpectIntEQ(pCtxLen, 2); + ExpectIntEQ(pHkdf, sha256_mac); + ExpectIntEQ(pProto.major, pv.major); + ExpectIntEQ(pProto.minor, pv.minor); + + /* Malformed: empty external_identity must be rejected (the historical + * NULL-deref case). */ + { + byte empty[] = { 0x00, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x01 }; + pId = NULL; + ExpectIntNE(TLSX_PreSharedKey_ParseImportedIdentity(empty, + (word16)sizeof(empty), &pId, &pIdLen, &pCtx, &pCtxLen, &pHkdf, + &pProto), 0); + } + + /* Malformed: total length below the minimum header is rejected. */ + ExpectIntNE(TLSX_PreSharedKey_ParseImportedIdentity(out, 7, &pId, &pIdLen, + &pCtx, &pCtxLen, &pHkdf, &pProto), 0); + + /* Malformed: identity length exceeds the buffer is rejected. */ + { + byte ovf[] = { 0xFF, 0xFF, 0x00, 0x00, 0x03, 0x04, 0x00, 0x01 }; + ExpectIntNE(TLSX_PreSharedKey_ParseImportedIdentity(ovf, + (word16)sizeof(ovf), &pId, &pIdLen, &pCtx, &pCtxLen, &pHkdf, + &pProto), 0); + } + + /* Malformed: unsupported target_kdf is rejected. */ + { + byte badKdf[sizeof(out)]; + XMEMCPY(badKdf, out, outLen); + badKdf[outLen - 1] = 0x09; /* not HKDF_SHA256/HKDF_SHA384 */ + ExpectIntNE(TLSX_PreSharedKey_ParseImportedIdentity(badKdf, outLen, + &pId, &pIdLen, &pCtx, &pCtxLen, &pHkdf, &pProto), 0); + } + return EXPECT_RESULT(); } #else @@ -30546,6 +30633,10 @@ static int test_tls13_external_psk_importer(void) { return TEST_SKIPPED; } +static int test_tls13_external_psk_importer_parse(void) +{ + return TEST_SKIPPED; +} #endif #if defined(WOLFSSL_TLS13) && defined(OPENSSL_EXTRA) && \ @@ -35680,6 +35771,7 @@ TEST_CASE testCases[] = { /* Can't memory test as client/server Asserts in thread. */ TEST_DECL(test_prioritize_psk), TEST_DECL(test_tls13_external_psk_importer), + TEST_DECL(test_tls13_external_psk_importer_parse), /* Can't memory test as client/server hangs. */ TEST_DECL(test_wc_CryptoCb), From 556dddef3a5779d426a471aeac69599858ad2a77 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 18:28:55 +0000 Subject: [PATCH 03/10] RFC 9258 importer: opaque length-delimited identity API Harden the importer callback contract so the external identity (and context) are opaque, length-delimited byte buffers instead of a NUL-terminated C string recovered with strlen. * Client callback signature is now (ssl, identity, *identitySz, context, *contextSz, key, *keySz): each *Sz holds the buffer capacity on entry and the actual length on exit. The strlen-based length recovery is removed, and identities may contain arbitrary bytes (RFC 9258 external_identity is opaque). * Server callback signature is now (ssl, identity, identitySz, context, contextSz, key, *keySz): identity and context are passed as explicit (ptr,len) pairs aliasing the received message. This removes the previous heap copy and NUL-termination of the received identity entirely. * Updated both client call sites, the server lookup in FindPskSuite, the test callbacks, and the API documentation accordingly. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- doc/dox_comments/header_files/ssl.h | 11 +++++++---- src/tls.c | 10 ++++++---- src/tls13.c | 24 ++++++++---------------- tests/api.c | 23 ++++++++++++++--------- wolfssl/ssl.h | 17 +++++++++++++---- 5 files changed, 48 insertions(+), 37 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index 9df96eb974c..aec84cf0c43 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15228,7 +15228,10 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per RFC 9258. The callback returns the external identity, an optional context and the external PSK; wolfSSL derives the ImportedIdentity and the imported - PSK used in the handshake. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + PSK used in the handshake. The identity, context and key are opaque byte + buffers: on entry each length argument holds the buffer capacity and the + callback overwrites it with the actual length. Requires + WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a client PSK importer callback. @@ -15277,9 +15280,9 @@ void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, \brief This function sets, on the WOLFSSL_CTX, the server side callback used to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per - RFC 9258. Given a received external identity and optional context, the - callback returns the matching external PSK. Requires - WOLFSSL_EXTERNAL_PSK_IMPORTER. + RFC 9258. Given a received external identity and optional context (each an + opaque buffer with an explicit length), the callback returns the matching + external PSK. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a server PSK importer callback. diff --git a/src/tls.c b/src/tls.c index 27565209d7d..9d11db589db 100644 --- a/src/tls.c +++ b/src/tls.c @@ -16643,7 +16643,9 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER if (ssl->options.client_psk_importer_cb != NULL) { int i; - word32 identitySz = 0; + /* On entry these hold the buffer capacities; the callback + * overwrites them with the actual opaque lengths. */ + word32 identitySz = MAX_PSK_ID_LEN; word32 ctxSz = MAX_PSK_CTX_LEN; byte* importedIdentity = NULL; /* Track which target KDFs already produced an identity so a @@ -16663,16 +16665,16 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) XMEMSET(seenKdf, 0, sizeof(seenKdf)); /* Get the external PSK data (identity, optional context and - * the actual key) by executing the user callback. */ + * the actual key) by executing the user callback. The identity + * is an opaque, length-delimited blob (RFC 9258). */ ret = ssl->options.client_psk_importer_cb(ssl, - ssl->arrays->client_identity, MAX_PSK_ID_LEN, ctx, + (byte*)ssl->arrays->client_identity, &identitySz, ctx, &ctxSz, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); if (ret != 0) { ret = PSK_KEY_ERROR; goto importer_cleanup; } - identitySz = XSTRLEN(ssl->arrays->client_identity); if (identitySz > MAX_PSK_ID_LEN || ctxSz > MAX_PSK_CTX_LEN || ssl->arrays->psk_keySz > MAX_PSK_KEY_LEN) { diff --git a/src/tls13.c b/src/tls13.c index cfbfae16e41..3ed4c967252 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4504,18 +4504,17 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello) #endif /* OPENSSL_EXTRA */ #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER if (ssl->options.client_psk_importer_cb != NULL) { - word32 identitySz = 0; + word32 identitySz = MAX_PSK_ID_LEN; ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; - /* Lookup key again for next identity. */ + /* Lookup key again for next identity. The context is not needed + * here, so its buffer and length are passed as NULL. */ ret = ssl->options.client_psk_importer_cb(ssl, - ssl->arrays->client_identity, MAX_PSK_ID_LEN, NULL, + (byte*)ssl->arrays->client_identity, &identitySz, NULL, NULL, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); if (ret != 0) return PSK_KEY_ERROR; - identitySz = XSTRLEN(ssl->arrays->client_identity); - if (identitySz > MAX_PSK_ID_LEN || ssl->arrays->psk_keySz > MAX_PSK_KEY_LEN) return PSK_KEY_ERROR; @@ -6570,20 +6569,13 @@ int FindPskSuite(const WOLFSSL* ssl, PreSharedKey* psk, byte* psk_key, SuiteMac(suite) == targetKdf && targetProtocol.major == ssl->version.major && targetProtocol.minor == ssl->version.minor) { - /* The importer callback expects a NUL-terminated identity. Copy - * it out instead of mutating the received message buffer. */ - char* idStr = (char*)XMALLOC((word32)idSz + 1, ssl->heap, - DYNAMIC_TYPE_TMP_BUFFER); - if (idStr == NULL) - return MEMORY_ERROR; - XMEMCPY(idStr, id, idSz); - idStr[idSz] = '\0'; *psk_keySz = MAX_PSK_KEY_LEN; - /* Look for an external PSK for that identity and context. */ + /* Look for an external PSK for that identity and context. The + * identity and context are passed as opaque (ptr,len) pairs that + * alias into the received message; no copy/termination required. */ ret = ssl->options.server_psk_importer_cb((WOLFSSL*)ssl, - (const char*)idStr, ctx, ctxSz, psk_key, psk_keySz); - XFREE(idStr, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + id, idSz, ctx, ctxSz, psk_key, psk_keySz); if (ret == 0 && *psk_keySz > 0 && *psk_keySz <= MAX_PSK_KEY_LEN) { /* Not yet set on the server-side */ diff --git a/tests/api.c b/tests/api.c index 062803b4f0d..7b098972f6f 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30415,14 +30415,17 @@ static int test_psk_importer_use_context = 0; /* When set, the server returns a different EPSK so the binder must not match. */ static int test_psk_importer_server_wrong_key = 0; -static int test_psk_importer_client_cb(WOLFSSL* ssl, char* id, word32 idMax, - unsigned char* ctx, word32* ctxSz, unsigned char* key, word32* keySz) +static int test_psk_importer_client_cb(WOLFSSL* ssl, unsigned char* id, + word32* idSz, unsigned char* ctx, word32* ctxSz, unsigned char* key, + word32* keySz) { + word32 idLen = (word32)XSTRLEN(test_psk_importer_identity); (void)ssl; - if (XSTRLEN(test_psk_importer_identity) + 1 > idMax) + if (idLen > *idSz) return -1; - XSTRNCPY(id, test_psk_importer_identity, idMax); + XMEMCPY(id, test_psk_importer_identity, idLen); + *idSz = idLen; if (ctx != NULL && ctxSz != NULL) { if (test_psk_importer_use_context) { @@ -30445,14 +30448,16 @@ static int test_psk_importer_client_cb(WOLFSSL* ssl, char* id, word32 idMax, return 0; } -static int test_psk_importer_server_cb(WOLFSSL* ssl, const char* id, - const unsigned char* ctx, word32 ctxSz, unsigned char* key, - word32* keySz) +static int test_psk_importer_server_cb(WOLFSSL* ssl, const unsigned char* id, + word32 idSz, const unsigned char* ctx, word32 ctxSz, + unsigned char* key, word32* keySz) { + word32 idLen = (word32)XSTRLEN(test_psk_importer_identity); (void)ssl; - /* Match the received external identity. */ - if (id == NULL || XSTRCMP(id, test_psk_importer_identity) != 0) + /* Match the received external identity (opaque, length-delimited). */ + if (id == NULL || idSz != idLen || + XMEMCMP(id, test_psk_importer_identity, idLen) != 0) return -1; /* Match the optional context exactly as advertised by the client. */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 6103c3080c5..be106e626b7 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -3282,8 +3282,12 @@ enum { /* ssl Constants */ wc_psk_client_tls13_callback cb); #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER - typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl, char*, - word32, unsigned char*, word32*, unsigned char*, word32*); + /* On entry each *Sz holds the buffer capacity; on exit the actual length. + * identity, context and key are opaque byte buffers (RFC 9258). */ + typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl, + unsigned char* identity, word32* identitySz, + unsigned char* context, word32* contextSz, + unsigned char* key, word32* keySz); WOLFSSL_API void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, wc_psk_client_importer_callback cb); WOLFSSL_API void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, @@ -3312,8 +3316,13 @@ enum { /* ssl Constants */ wc_psk_server_tls13_callback cb); #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER - typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl, const char*, - const unsigned char*, word32, unsigned char*, word32*); + /* identity and context are opaque (ptr,len) pairs as received on the wire; + * on entry *keySz holds the key buffer capacity, on exit the actual key + * length (RFC 9258). */ + typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl, + const unsigned char* identity, word32 identitySz, + const unsigned char* context, word32 contextSz, + unsigned char* key, word32* keySz); WOLFSSL_API void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, wc_psk_server_importer_callback cb); WOLFSSL_API void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, From 0f8083a25a93dcd25cfa016054323e06b57355aa Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 19:12:42 +0000 Subject: [PATCH 04/10] RFC 9258 importer: configurable EPSK hash function RFC 9258 derives the imported PSK using the hash associated with the external PSK (defaulting to SHA-256 only when none is associated). The importer hash was previously hardcoded to SHA-256, which is non-compliant for an EPSK associated with a different hash (e.g. SHA-384). Expose the EPSK hash through the importer callbacks via an int* hashAlgo argument that is pre-initialized to WC_SHA256: callers that use the default need not touch it, and only set it for an EPSK associated with another hash. * Both client and server importer callbacks gain a trailing int* hashAlgo. * DeriveImportedPreSharedKey() takes the importer hash and uses it for both Hash(ImportedIdentity) and the HKDF-Extract/Expand, via the generic wc_Hash / wc_HKDF_*_ex APIs (buffers sized to WC_MAX_DIGEST_SIZE). The digest size is validated to fit the buffers. The target_kdf still solely determines the imported-PSK output length L, independent of this hash. * The importer hash is threaded straight from each callback invocation to the derivation (both call sites already call the callback immediately before deriving), so no extra state is stored. * Tests: callbacks advertise the hash; added a case with a SHA-384 associated EPSK used with a SHA-256 target_kdf. * Updated API documentation. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- doc/dox_comments/header_files/ssl.h | 10 +++-- src/tls.c | 7 ++- src/tls13.c | 67 +++++++++++++---------------- tests/api.c | 25 ++++++++++- wolfssl/ssl.h | 13 ++++-- 5 files changed, 74 insertions(+), 48 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index aec84cf0c43..e632d482fa6 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15230,8 +15230,10 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, and the external PSK; wolfSSL derives the ImportedIdentity and the imported PSK used in the handshake. The identity, context and key are opaque byte buffers: on entry each length argument holds the buffer capacity and the - callback overwrites it with the actual length. Requires - WOLFSSL_EXTERNAL_PSK_IMPORTER. + callback overwrites it with the actual length. The hashAlgo argument is the + hash associated with the external PSK; it is pre-set to WC_SHA256 (the + RFC 9258 default) and only needs to be changed for an EPSK associated with + a different hash. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a client PSK importer callback. @@ -15282,7 +15284,9 @@ void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, to import an external Pre-Shared Key (PSK) for TLS v1.3 connections per RFC 9258. Given a received external identity and optional context (each an opaque buffer with an explicit length), the callback returns the matching - external PSK. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + external PSK. The hashAlgo argument is the hash associated with the external + PSK; it is pre-set to WC_SHA256 and must be set to the same value the client + used for that EPSK. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a server PSK importer callback. diff --git a/src/tls.c b/src/tls.c index 9d11db589db..95b2807031d 100644 --- a/src/tls.c +++ b/src/tls.c @@ -16647,6 +16647,10 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) * overwrites them with the actual opaque lengths. */ word32 identitySz = MAX_PSK_ID_LEN; word32 ctxSz = MAX_PSK_CTX_LEN; + /* EPSK hash (RFC 9258 default SHA-256). Only the identity is + * built here; the actual import derivation, which uses this + * hash, happens later in SetupPskKey(). */ + int importerHash = WC_SHA256; byte* importedIdentity = NULL; /* Track which target KDFs already produced an identity so a * single KDF is not emitted more than once (RFC 9258 derives @@ -16669,7 +16673,8 @@ int TLSX_PopulateExtensions(WOLFSSL* ssl, byte isServer) * is an opaque, length-delimited blob (RFC 9258). */ ret = ssl->options.client_psk_importer_cb(ssl, (byte*)ssl->arrays->client_identity, &identitySz, ctx, - &ctxSz, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); + &ctxSz, ssl->arrays->psk_key, &ssl->arrays->psk_keySz, + &importerHash); if (ret != 0) { ret = PSK_KEY_ERROR; goto importer_cleanup; diff --git a/src/tls13.c b/src/tls13.c index 3ed4c967252..54a905b571f 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -1214,23 +1214,22 @@ static const byte derivedPskLabel[DERIVED_PSK_LABEL_SZ + 1] = * * ssl The SSL/TLS object. */ -static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) +static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, + int importerHash) { int ret; const byte* protocol; word32 protocolLen; word32 outputLen; - word32 hashSz = 0; - byte hash[WC_SHA256_DIGEST_SIZE]; - byte prk[WC_SHA256_DIGEST_SIZE]; + int hashSz = 0; + byte hash[WC_MAX_DIGEST_SIZE]; + byte prk[WC_MAX_DIGEST_SIZE]; byte okm[MAX_PSK_KEY_LEN]; word32 idx = 0; #ifdef WOLFSSL_SMALL_STACK byte* hkdfLabel = NULL; - wc_Sha256* sha256 = NULL; #else byte hkdfLabel[MAX_TLS13_HKDF_LABEL_SZ]; - wc_Sha256 sha256[1]; #endif WOLFSSL_MSG("Derive Imported Pre-shared Key"); @@ -1238,35 +1237,21 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) return BAD_FUNC_ARG; } - /* Currently, SHA-256 is used as the underlying hash function for the HKDF - * operations below, following RFC 9258. In the future, the callback - * interface could be expanded to let the user provide a different hash - * algorithm. */ - -#ifdef WOLFSSL_SMALL_STACK - sha256 = (wc_Sha256*)XMALLOC(sizeof(wc_Sha256), ssl->heap, - DYNAMIC_TYPE_HASH_TMP); - if (sha256 == NULL) { - return MEMORY_E; - } -#endif + /* The hash function used for the import (Hash(ImportedIdentity) and the + * HKDF below) is the one associated with the external PSK, defaulting to + * SHA-256 per RFC 9258. It is independent of the target_kdf, which only + * determines the imported-PSK output length L. Validate that the digest + * fits our buffers and is a supported hash. */ + hashSz = wc_HashGetDigestSize((enum wc_HashType)importerHash); + if (hashSz <= 0 || hashSz > (int)sizeof(hash)) + return BAD_FUNC_ARG; /* Create the hash of the ImportedIdentity */ - ret = wc_InitSha256_ex(sha256, ssl->heap, ssl->devId); - if (ret == 0) { - ret = wc_Sha256Update(sha256, psk->identity, psk->identityLen); - if (ret == 0) - ret = wc_Sha256Final(sha256, hash); - wc_Sha256Free(sha256); - } -#ifdef WOLFSSL_SMALL_STACK - XFREE(sha256, ssl->heap, DYNAMIC_TYPE_HASH_TMP); -#endif + ret = wc_Hash((enum wc_HashType)importerHash, psk->identity, + psk->identityLen, hash, (word32)hashSz); if (ret != 0) return ret; - hashSz = WC_SHA256_DIGEST_SIZE; - switch (ssl->version.minor) { case TLSv1_3_MINOR: protocol = tls13ProtocolLabel; @@ -1345,7 +1330,7 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) /* The external PSK is already a pseudorandom key (the result of an * earlier HKDF-Extract), so derive ipskx directly with an * HKDF-Expand-Label. */ - ret = wc_HKDF_Expand_ex(WC_SHA256, ssl->arrays->psk_key, + ret = wc_HKDF_Expand_ex(importerHash, ssl->arrays->psk_key, ssl->arrays->psk_keySz, hkdfLabel, idx, okm, outputLen, ssl->heap, ssl->devId); } @@ -1353,16 +1338,16 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk) /* epskx = HKDF-Extract(0, epsk) */ #if !defined(HAVE_FIPS) || \ (defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)) - ret = wc_HKDF_Extract_ex(WC_SHA256, NULL, 0, ssl->arrays->psk_key, + ret = wc_HKDF_Extract_ex(importerHash, NULL, 0, ssl->arrays->psk_key, ssl->arrays->psk_keySz, prk, ssl->heap, ssl->devId); #else - ret = wc_HKDF_Extract(WC_SHA256, NULL, 0, ssl->arrays->psk_key, + ret = wc_HKDF_Extract(importerHash, NULL, 0, ssl->arrays->psk_key, ssl->arrays->psk_keySz, prk); #endif if (ret == 0) { /* ipskx = HKDF-Expand-Label(epskx, "derived psk", * Hash(ImportedIdentity), L) */ - ret = wc_HKDF_Expand_ex(WC_SHA256, prk, WC_SHA256_DIGEST_SIZE, + ret = wc_HKDF_Expand_ex(importerHash, prk, (word32)hashSz, hkdfLabel, idx, okm, outputLen, ssl->heap, ssl->devId); } } @@ -4505,13 +4490,16 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello) #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER if (ssl->options.client_psk_importer_cb != NULL) { word32 identitySz = MAX_PSK_ID_LEN; + /* Hash associated with the EPSK; defaults to SHA-256 (RFC 9258). */ + int importerHash = WC_SHA256; ssl->arrays->psk_keySz = MAX_PSK_KEY_LEN; /* Lookup key again for next identity. The context is not needed * here, so its buffer and length are passed as NULL. */ ret = ssl->options.client_psk_importer_cb(ssl, (byte*)ssl->arrays->client_identity, &identitySz, NULL, - NULL, ssl->arrays->psk_key, &ssl->arrays->psk_keySz); + NULL, ssl->arrays->psk_key, &ssl->arrays->psk_keySz, + &importerHash); if (ret != 0) return PSK_KEY_ERROR; @@ -4541,7 +4529,7 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello) } /* Derive the imported PSK */ - ret = DeriveImportedPreSharedKey(ssl, psk); + ret = DeriveImportedPreSharedKey(ssl, psk, importerHash); if (ret != 0) return ret; } @@ -6556,6 +6544,8 @@ int FindPskSuite(const WOLFSSL* ssl, PreSharedKey* psk, byte* psk_key, word16 ctxSz = 0; ProtocolVersion targetProtocol; byte targetKdf = 0; + /* Hash associated with the EPSK; defaults to SHA-256 (RFC 9258). */ + int importerHash = WC_SHA256; XMEMSET(&targetProtocol, 0, sizeof(targetProtocol)); @@ -6575,14 +6565,15 @@ int FindPskSuite(const WOLFSSL* ssl, PreSharedKey* psk, byte* psk_key, * identity and context are passed as opaque (ptr,len) pairs that * alias into the received message; no copy/termination required. */ ret = ssl->options.server_psk_importer_cb((WOLFSSL*)ssl, - id, idSz, ctx, ctxSz, psk_key, psk_keySz); + id, idSz, ctx, ctxSz, psk_key, psk_keySz, &importerHash); if (ret == 0 && *psk_keySz > 0 && *psk_keySz <= MAX_PSK_KEY_LEN) { /* Not yet set on the server-side */ psk->hmac = targetKdf; /* Derive the actual imported PSK */ - ret = DeriveImportedPreSharedKey((WOLFSSL*)ssl, psk); + ret = DeriveImportedPreSharedKey((WOLFSSL*)ssl, psk, + importerHash); if (ret == 0) { *found = 1; diff --git a/tests/api.c b/tests/api.c index 7b098972f6f..dc9e2a6269c 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30414,10 +30414,13 @@ static const char test_psk_importer_context[] = "RFC 9258 test context"; static int test_psk_importer_use_context = 0; /* When set, the server returns a different EPSK so the binder must not match. */ static int test_psk_importer_server_wrong_key = 0; +/* Hash associated with the EPSK that both callbacks advertise (default SHA-256 + * per RFC 9258; set to e.g. WC_SHA384 to exercise a non-default importer). */ +static int test_psk_importer_hash = WC_SHA256; static int test_psk_importer_client_cb(WOLFSSL* ssl, unsigned char* id, word32* idSz, unsigned char* ctx, word32* ctxSz, unsigned char* key, - word32* keySz) + word32* keySz, int* hashAlgo) { word32 idLen = (word32)XSTRLEN(test_psk_importer_identity); (void)ssl; @@ -30445,12 +30448,16 @@ static int test_psk_importer_client_cb(WOLFSSL* ssl, unsigned char* id, XMEMCPY(key, test_psk_importer_epsk, sizeof(test_psk_importer_epsk)); *keySz = (word32)sizeof(test_psk_importer_epsk); + /* hashAlgo arrives pre-set to WC_SHA256; only override when testing a + * non-default EPSK hash. */ + *hashAlgo = test_psk_importer_hash; + return 0; } static int test_psk_importer_server_cb(WOLFSSL* ssl, const unsigned char* id, word32 idSz, const unsigned char* ctx, word32 ctxSz, - unsigned char* key, word32* keySz) + unsigned char* key, word32* keySz, int* hashAlgo) { word32 idLen = (word32)XSTRLEN(test_psk_importer_identity); (void)ssl; @@ -30471,6 +30478,9 @@ static int test_psk_importer_server_cb(WOLFSSL* ssl, const unsigned char* id, return -1; } + /* Advertise the same EPSK hash as the client (default SHA-256). */ + *hashAlgo = test_psk_importer_hash; + if ((word32)sizeof(test_psk_importer_epsk) > *keySz) return -1; XMEMCPY(key, test_psk_importer_epsk, sizeof(test_psk_importer_epsk)); @@ -30542,6 +30552,7 @@ static int test_tls13_external_psk_importer(void) EXPECT_DECLS; test_psk_importer_server_wrong_key = 0; + test_psk_importer_hash = WC_SHA256; /* HKDF_SHA256 target_kdf, without and with an optional context. */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", @@ -30557,6 +30568,16 @@ static int test_tls13_external_psk_importer(void) 0x1302, 1, 0, 0), TEST_SUCCESS); #endif +#if defined(WOLFSSL_SHA384) + /* EPSK associated with SHA-384 (non-default importer hash), used with a + * SHA-256 target_kdf: exercises an importer hash that differs from the + * target KDF (RFC 9258 Section 3.1). */ + test_psk_importer_hash = WC_SHA384; + ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", + 0x1301, 1, 0, 0), TEST_SUCCESS); + test_psk_importer_hash = WC_SHA256; +#endif + /* Negative: server derives a different imported PSK -> binder mismatch * -> handshake must fail. */ test_psk_importer_server_wrong_key = 1; diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index be106e626b7..77b81251e28 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -3283,11 +3283,14 @@ enum { /* ssl Constants */ #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER /* On entry each *Sz holds the buffer capacity; on exit the actual length. - * identity, context and key are opaque byte buffers (RFC 9258). */ + * identity, context and key are opaque byte buffers (RFC 9258). + * hashAlgo is the hash function associated with the external PSK: it is + * pre-initialized to WC_SHA256 (the RFC 9258 default) and only needs to be + * changed if the EPSK is associated with a different hash. */ typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl, unsigned char* identity, word32* identitySz, unsigned char* context, word32* contextSz, - unsigned char* key, word32* keySz); + unsigned char* key, word32* keySz, int* hashAlgo); WOLFSSL_API void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, wc_psk_client_importer_callback cb); WOLFSSL_API void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, @@ -3318,11 +3321,13 @@ enum { /* ssl Constants */ #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER /* identity and context are opaque (ptr,len) pairs as received on the wire; * on entry *keySz holds the key buffer capacity, on exit the actual key - * length (RFC 9258). */ + * length (RFC 9258). hashAlgo is the hash associated with the external PSK, + * pre-initialized to WC_SHA256; change it only for an EPSK associated with + * a different hash. */ typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl, const unsigned char* identity, word32 identitySz, const unsigned char* context, word32 contextSz, - unsigned char* key, word32* keySz); + unsigned char* key, word32* keySz, int* hashAlgo); WOLFSSL_API void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, wc_psk_server_importer_callback cb); WOLFSSL_API void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, From 7add5ea72ed94b291ad71b5089ce14a518602ee1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 19:16:40 +0000 Subject: [PATCH 05/10] RFC 9258 importer: offload ImportedIdentity hash via devId Compute Hash(ImportedIdentity) with the streaming wc_HashAlg API (wc_HashInit_ex/Update/Final/Free) instead of the one-shot wc_Hash, so the hash can be offloaded through devId/CryptoCb like the HKDF steps already are. The hash context is stack-allocated normally and heap-allocated under WOLFSSL_SMALL_STACK. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- src/tls13.c | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/tls13.c b/src/tls13.c index 54a905b571f..b6676a337f1 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -1228,8 +1228,10 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, word32 idx = 0; #ifdef WOLFSSL_SMALL_STACK byte* hkdfLabel = NULL; + wc_HashAlg* hashAlg = NULL; #else byte hkdfLabel[MAX_TLS13_HKDF_LABEL_SZ]; + wc_HashAlg hashAlg[1]; #endif WOLFSSL_MSG("Derive Imported Pre-shared Key"); @@ -1246,9 +1248,26 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, if (hashSz <= 0 || hashSz > (int)sizeof(hash)) return BAD_FUNC_ARG; - /* Create the hash of the ImportedIdentity */ - ret = wc_Hash((enum wc_HashType)importerHash, psk->identity, - psk->identityLen, hash, (word32)hashSz); +#ifdef WOLFSSL_SMALL_STACK + hashAlg = (wc_HashAlg*)XMALLOC(sizeof(wc_HashAlg), ssl->heap, + DYNAMIC_TYPE_HASHES); + if (hashAlg == NULL) + return MEMORY_E; +#endif + + /* Create the hash of the ImportedIdentity, offloading to devId if set. */ + ret = wc_HashInit_ex(hashAlg, (enum wc_HashType)importerHash, ssl->heap, + ssl->devId); + if (ret == 0) { + ret = wc_HashUpdate(hashAlg, (enum wc_HashType)importerHash, + psk->identity, psk->identityLen); + if (ret == 0) + ret = wc_HashFinal(hashAlg, (enum wc_HashType)importerHash, hash); + wc_HashFree(hashAlg, (enum wc_HashType)importerHash); + } +#ifdef WOLFSSL_SMALL_STACK + XFREE(hashAlg, ssl->heap, DYNAMIC_TYPE_HASHES); +#endif if (ret != 0) return ret; From f992fcb0e16f379fff096a43fb63c68ca652f44c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 19:42:50 +0000 Subject: [PATCH 06/10] RFC 9258 importer: known-answer test, docs, testable derivation core Address review feedback on PR #27. * Refactor the imported-PSK derivation into a WOLFSSL-independent core, DeriveImportedPsk(), exposed as WOLFSSL_LOCAL. DeriveImportedPreSharedKey() becomes a thin wrapper that sources its inputs from the SSL object. This lets the derivation be exercised directly with known-answer vectors. * Add test_tls13_external_psk_importer_kat: known-answer tests for the ImportedIdentity serialization and the ipskx output. The expected values were computed independently of wolfSSL (Python hashlib/hmac per RFC 9258 Section 3.1), so a symmetric derivation bug that the handshake tests would miss is now caught. Three vectors cover SHA-256/SHA-384 target KDFs and a non-default (SHA-384) importer hash. * Document the importer callback contract: callbacks must be deterministic (invoked more than once per connection), and the identity/context size caps (MAX_PSK_ID_LEN / MAX_PSK_CTX_LEN), in both ssl.h and the Doxygen docs. No standalone interoperable reference implementation of the RFC 9258 importer transform exists publicly (the RFC ships no test vectors), so the independently-computed KAT is used in place of cross-implementation interop. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- doc/dox_comments/header_files/ssl.h | 17 ++- src/tls13.c | 169 +++++++++++++++++----------- tests/api.c | 112 ++++++++++++++++++ wolfssl/internal.h | 5 + wolfssl/ssl.h | 11 +- 5 files changed, 241 insertions(+), 73 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index e632d482fa6..a2cecd00851 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15230,10 +15230,14 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, and the external PSK; wolfSSL derives the ImportedIdentity and the imported PSK used in the handshake. The identity, context and key are opaque byte buffers: on entry each length argument holds the buffer capacity and the - callback overwrites it with the actual length. The hashAlgo argument is the - hash associated with the external PSK; it is pre-set to WC_SHA256 (the - RFC 9258 default) and only needs to be changed for an EPSK associated with - a different hash. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + callback overwrites it with the actual length. The context is limited to + MAX_PSK_CTX_LEN bytes and the identity to MAX_PSK_ID_LEN bytes. The hashAlgo + argument is the hash associated with the external PSK; it is pre-set to + WC_SHA256 (the RFC 9258 default) and only needs to be changed for an EPSK + associated with a different hash. The callback must be deterministic: it is + invoked more than once per connection (when building the ClientHello and + again while computing the binders) and must return the same identity, + context, key and hash each time. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a client PSK importer callback. @@ -15286,7 +15290,10 @@ void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, opaque buffer with an explicit length), the callback returns the matching external PSK. The hashAlgo argument is the hash associated with the external PSK; it is pre-set to WC_SHA256 and must be set to the same value the client - used for that EPSK. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. + used for that EPSK. The callback must be deterministic: it may be invoked + more than once per connection (once per candidate cipher suite) and must + return the same key and hash each time. Requires + WOLFSSL_EXTERNAL_PSK_IMPORTER. \param [in,out] ctx a pointer to a WOLFSSL_CTX structure. \param [in] cb a server PSK importer callback. diff --git a/src/tls13.c b/src/tls13.c index b6676a337f1..cad25f05153 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -1209,23 +1209,42 @@ static int Tls13_HKDF_Extract(WOLFSSL *ssl, byte* prk, const byte* salt, static const byte derivedPskLabel[DERIVED_PSK_LABEL_SZ + 1] = "derived psk"; -/* Derive the imported PSK key from the external one and the created - * ImportedIdentity. +/* Derive an imported PSK from an external PSK and its ImportedIdentity + * (RFC 9258, Section 3.1). This core routine is independent of WOLFSSL state so + * it can be exercised directly with known-answer test vectors. * - * ssl The SSL/TLS object. + * epsk/epskSz External PSK base key (or a pre-extracted PRK). + * preExtracted Non-zero if epsk is already a PRK (skip HKDF-Extract). + * importedIdentity Serialized ImportedIdentity (hashed as the context). + * importedIdentitySz Length of importedIdentity. + * importerHash Hash associated with the EPSK (e.g. WC_SHA256), used for + * Hash(ImportedIdentity) and the HKDF. Independent of the + * target KDF. + * targetKdfMac MAC algorithm of the target_kdf; sets the output length + * L (the digest size of that KDF's hash). + * protocolMinor (D)TLS minor version, selecting the protocol label. + * isDtls Non-zero for DTLS 1.3. + * out Receives ipskx; must hold at least L bytes. May alias + * epsk (the input is fully consumed before out is written). + * outSz On exit, set to L. + * + * Returns 0 on success, otherwise a negative error. */ -static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, - int importerHash) +WOLFSSL_LOCAL int DeriveImportedPsk(const byte* epsk, word32 epskSz, + int preExtracted, const byte* importedIdentity, + word32 importedIdentitySz, int importerHash, byte targetKdfMac, + byte protocolMinor, int isDtls, byte* out, word32* outSz, + void* heap, int devId) { int ret; const byte* protocol; word32 protocolLen; word32 outputLen; - int hashSz = 0; + int hashSz; byte hash[WC_MAX_DIGEST_SIZE]; byte prk[WC_MAX_DIGEST_SIZE]; byte okm[MAX_PSK_KEY_LEN]; - word32 idx = 0; + word32 idx; #ifdef WOLFSSL_SMALL_STACK byte* hkdfLabel = NULL; wc_HashAlg* hashAlg = NULL; @@ -1235,9 +1254,9 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, #endif WOLFSSL_MSG("Derive Imported Pre-shared Key"); - if (ssl == NULL || ssl->arrays == NULL) { + if (epsk == NULL || importedIdentity == NULL || out == NULL || + outSz == NULL) return BAD_FUNC_ARG; - } /* The hash function used for the import (Hash(ImportedIdentity) and the * HKDF below) is the one associated with the external PSK, defaulting to @@ -1248,39 +1267,23 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, if (hashSz <= 0 || hashSz > (int)sizeof(hash)) return BAD_FUNC_ARG; -#ifdef WOLFSSL_SMALL_STACK - hashAlg = (wc_HashAlg*)XMALLOC(sizeof(wc_HashAlg), ssl->heap, - DYNAMIC_TYPE_HASHES); - if (hashAlg == NULL) - return MEMORY_E; -#endif - - /* Create the hash of the ImportedIdentity, offloading to devId if set. */ - ret = wc_HashInit_ex(hashAlg, (enum wc_HashType)importerHash, ssl->heap, - ssl->devId); - if (ret == 0) { - ret = wc_HashUpdate(hashAlg, (enum wc_HashType)importerHash, - psk->identity, psk->identityLen); - if (ret == 0) - ret = wc_HashFinal(hashAlg, (enum wc_HashType)importerHash, hash); - wc_HashFree(hashAlg, (enum wc_HashType)importerHash); - } -#ifdef WOLFSSL_SMALL_STACK - XFREE(hashAlg, ssl->heap, DYNAMIC_TYPE_HASHES); -#endif - if (ret != 0) + /* The output length L matches the digest size of the target_kdf. */ + ret = wc_HashGetDigestSize(mac2hash(targetKdfMac)); + if (ret < 0) return ret; + outputLen = (word32)ret; + if (outputLen == 0 || outputLen > (word32)sizeof(okm)) + return BUFFER_E; - switch (ssl->version.minor) { + switch (protocolMinor) { case TLSv1_3_MINOR: protocol = tls13ProtocolLabel; protocolLen = TLS13_PROTOCOL_LABEL_SZ; break; #ifdef WOLFSSL_DTLS13 case DTLSv1_3_MINOR: - if (!ssl->options.dtls) + if (!isDtls) return VERSION_ERROR; - protocol = dtls13ProtocolLabel; protocolLen = DTLS13_PROTOCOL_LABEL_SZ; break; @@ -1288,30 +1291,39 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, default: return VERSION_ERROR; } + (void)isDtls; - /* The output length of the of the HDKF-Expand-Label operation below must - * match the hash-function of the KDF used further on in the handshake. - * This is encoded in psk->hmac, as this contains the target_kdf of the - * ImportedIdentity. */ - ret = wc_HashGetDigestSize(mac2hash(psk->hmac)); - if (ret < 0) - return ret; - else { - outputLen = (word32)ret; - ret = 0; +#ifdef WOLFSSL_SMALL_STACK + hashAlg = (wc_HashAlg*)XMALLOC(sizeof(wc_HashAlg), heap, + DYNAMIC_TYPE_HASHES); + if (hashAlg == NULL) + return MEMORY_E; +#endif + + /* Create the hash of the ImportedIdentity, offloading to devId if set. */ + ret = wc_HashInit_ex(hashAlg, (enum wc_HashType)importerHash, heap, devId); + if (ret == 0) { + ret = wc_HashUpdate(hashAlg, (enum wc_HashType)importerHash, + importedIdentity, importedIdentitySz); + if (ret == 0) + ret = wc_HashFinal(hashAlg, (enum wc_HashType)importerHash, hash); + wc_HashFree(hashAlg, (enum wc_HashType)importerHash); } - if (outputLen > (word32)sizeof(okm)) - return BUFFER_E; +#ifdef WOLFSSL_SMALL_STACK + XFREE(hashAlg, heap, DYNAMIC_TYPE_HASHES); +#endif + if (ret != 0) + return ret; /* Check HkdfLabel length: okmLen (2) + protocol|label len (1) + * info len(1) + protocollen + labellen + infolen */ - idx = 4 + protocolLen + DERIVED_PSK_LABEL_SZ + hashSz; + idx = 4 + protocolLen + DERIVED_PSK_LABEL_SZ + (word32)hashSz; if (idx > MAX_TLS13_HKDF_LABEL_SZ) return BUFFER_E; #ifdef WOLFSSL_SMALL_STACK - hkdfLabel = (byte*)XMALLOC(idx, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + hkdfLabel = (byte*)XMALLOC(idx, heap, DYNAMIC_TYPE_TMP_BUFFER); if (hkdfLabel == NULL) ret = MEMORY_E; #endif @@ -1333,48 +1345,46 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, /* Length of hash */ hkdfLabel[idx++] = (byte)hashSz; /* Hash of messages */ - XMEMCPY(&hkdfLabel[idx], hash, hashSz); - idx += hashSz; + XMEMCPY(&hkdfLabel[idx], hash, (word32)hashSz); + idx += (word32)hashSz; #ifdef WOLFSSL_CHECK_MEM_ZERO - wc_MemZero_Add("DeriveImportedPreSharedKey hkdfLabel", hkdfLabel, idx); - wc_MemZero_Add("DeriveImportedPreSharedKey okm", okm, sizeof(okm)); - wc_MemZero_Add("DeriveImportedPreSharedKey prk", prk, sizeof(prk)); + wc_MemZero_Add("DeriveImportedPsk hkdfLabel", hkdfLabel, idx); + wc_MemZero_Add("DeriveImportedPsk okm", okm, sizeof(okm)); + wc_MemZero_Add("DeriveImportedPsk prk", prk, sizeof(prk)); #endif /* okm holds ipskx; prk holds epskx. Both are dedicated buffers so the - * input and output of the HKDF operations never alias psk_key. */ + * HKDF input may safely alias the output buffer. */ PRIVATE_KEY_UNLOCK(); - if (ssl->arrays->psk_externalKeyPreExtracted) { + if (preExtracted) { /* The external PSK is already a pseudorandom key (the result of an * earlier HKDF-Extract), so derive ipskx directly with an * HKDF-Expand-Label. */ - ret = wc_HKDF_Expand_ex(importerHash, ssl->arrays->psk_key, - ssl->arrays->psk_keySz, hkdfLabel, idx, okm, outputLen, - ssl->heap, ssl->devId); + ret = wc_HKDF_Expand_ex(importerHash, epsk, epskSz, hkdfLabel, idx, + okm, outputLen, heap, devId); } else { /* epskx = HKDF-Extract(0, epsk) */ #if !defined(HAVE_FIPS) || \ (defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)) - ret = wc_HKDF_Extract_ex(importerHash, NULL, 0, ssl->arrays->psk_key, - ssl->arrays->psk_keySz, prk, ssl->heap, ssl->devId); + ret = wc_HKDF_Extract_ex(importerHash, NULL, 0, epsk, epskSz, prk, + heap, devId); #else - ret = wc_HKDF_Extract(importerHash, NULL, 0, ssl->arrays->psk_key, - ssl->arrays->psk_keySz, prk); + ret = wc_HKDF_Extract(importerHash, NULL, 0, epsk, epskSz, prk); #endif if (ret == 0) { /* ipskx = HKDF-Expand-Label(epskx, "derived psk", * Hash(ImportedIdentity), L) */ ret = wc_HKDF_Expand_ex(importerHash, prk, (word32)hashSz, - hkdfLabel, idx, okm, outputLen, ssl->heap, ssl->devId); + hkdfLabel, idx, okm, outputLen, heap, devId); } } PRIVATE_KEY_LOCK(); if (ret == 0) { - XMEMCPY(ssl->arrays->psk_key, okm, outputLen); - ssl->arrays->psk_keySz = outputLen; + XMEMCPY(out, okm, outputLen); + *outSz = outputLen; } ForceZero(okm, sizeof(okm)); @@ -1389,11 +1399,40 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, } #ifdef WOLFSSL_SMALL_STACK - XFREE(hkdfLabel, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(hkdfLabel, heap, DYNAMIC_TYPE_TMP_BUFFER); #endif return ret; } + +/* Derive the imported PSK for a connection, sourcing the inputs from the SSL + * object and writing the result back into the PSK key buffer. + * + * ssl The SSL/TLS object. + * psk The PSK whose serialized ImportedIdentity and target_kdf drive + * the derivation. + * importerHash The hash associated with the external PSK (default WC_SHA256). + */ +static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, + int importerHash) +{ + int ret; + word32 keySz; + + if (ssl == NULL || ssl->arrays == NULL || psk == NULL) + return BAD_FUNC_ARG; + + keySz = ssl->arrays->psk_keySz; + ret = DeriveImportedPsk(ssl->arrays->psk_key, ssl->arrays->psk_keySz, + ssl->arrays->psk_externalKeyPreExtracted, psk->identity, + psk->identityLen, importerHash, psk->hmac, ssl->version.minor, + ssl->options.dtls, ssl->arrays->psk_key, &keySz, ssl->heap, + ssl->devId); + if (ret == 0) + ssl->arrays->psk_keySz = keySz; + + return ret; +} #endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ /* Derive the early secret using HKDF Extract. diff --git a/tests/api.c b/tests/api.c index dc9e2a6269c..080275fed83 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30654,6 +30654,113 @@ static int test_tls13_external_psk_importer_parse(void) return EXPECT_RESULT(); } + +/* Known-answer vectors for the imported-PSK derivation, computed independently + * of wolfSSL (Python hashlib/hmac) from RFC 9258 Section 3.1. A symmetric + * derivation bug would still let the handshake tests pass, so these pin the + * exact ImportedIdentity serialization and ipskx output. + * + * Inputs: external_identity = "9258 Client_identity", epsk = 16 x 0x0b, + * target_protocol = TLS 1.3 (0x0304). */ +static const byte test_kat_id[] = "9258 Client_identity"; /* 20 bytes, no NUL */ +static const byte test_kat_ctx[] = "RFC 9258 test context"; /* 21 bytes */ +static const byte test_kat_epsk[16] = { + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, + 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b +}; + +/* V1: no context, target_kdf = HKDF_SHA256 (L=32), importer hash SHA-256. */ +static const byte test_kat_ii1[] = { + 0x00, 0x14, 0x39, 0x32, 0x35, 0x38, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x00, 0x00, + 0x03, 0x04, 0x00, 0x01 +}; +static const byte test_kat_ipsk1[] = { + 0x03, 0xa2, 0x4c, 0xc5, 0xe7, 0x8a, 0xeb, 0xb0, 0x03, 0x22, 0x7b, 0x99, + 0x98, 0x3a, 0x66, 0x9c, 0x35, 0xa5, 0x98, 0x93, 0xb6, 0xd0, 0x57, 0x70, + 0x8d, 0xe3, 0x50, 0x50, 0x58, 0x1b, 0x59, 0xfe +}; +#if defined(WOLFSSL_SHA384) +/* V2: with context, target_kdf = HKDF_SHA384 (L=48), importer hash SHA-256. */ +static const byte test_kat_ii2[] = { + 0x00, 0x14, 0x39, 0x32, 0x35, 0x38, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x00, 0x15, + 0x52, 0x46, 0x43, 0x20, 0x39, 0x32, 0x35, 0x38, 0x20, 0x74, 0x65, 0x73, + 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x03, 0x04, 0x00, + 0x02 +}; +static const byte test_kat_ipsk2[] = { + 0x89, 0x27, 0x92, 0x54, 0xf5, 0x07, 0xa5, 0x5d, 0xeb, 0xff, 0x72, 0x4a, + 0xc1, 0xee, 0x14, 0x2a, 0x1f, 0x2d, 0xe7, 0x6d, 0x54, 0x18, 0xd7, 0x12, + 0xb4, 0xe9, 0x83, 0xdc, 0x4e, 0xd0, 0x71, 0x4e, 0x5b, 0x70, 0xa4, 0x77, + 0x78, 0x65, 0x09, 0x0c, 0x2e, 0x02, 0x61, 0x43, 0x6e, 0x2d, 0x75, 0x95 +}; +/* V3: with context, target_kdf = HKDF_SHA256 (L=32), importer hash SHA-384. */ +static const byte test_kat_ii3[] = { + 0x00, 0x14, 0x39, 0x32, 0x35, 0x38, 0x20, 0x43, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x00, 0x15, + 0x52, 0x46, 0x43, 0x20, 0x39, 0x32, 0x35, 0x38, 0x20, 0x74, 0x65, 0x73, + 0x74, 0x20, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x03, 0x04, 0x00, + 0x01 +}; +static const byte test_kat_ipsk3[] = { + 0xf2, 0x32, 0xfc, 0xfe, 0xdc, 0x36, 0x8b, 0xda, 0xf6, 0xbc, 0x4c, 0xcf, + 0xff, 0x4e, 0x60, 0xee, 0x2c, 0xa0, 0xd2, 0x81, 0x0a, 0x3d, 0x8c, 0x17, + 0xba, 0x4e, 0xac, 0x25, 0x13, 0x18, 0x7d, 0xd8 +}; +#endif /* WOLFSSL_SHA384 */ + +static int test_tls13_psk_importer_kat_one(const byte* ctx, word16 ctxLen, + byte targetKdfMac, int importerHash, const byte* expII, word16 expIISz, + const byte* expIpsk, word32 expIpskSz) +{ + EXPECT_DECLS; + byte ii[128]; + word16 iiSz = (word16)sizeof(ii); + byte out[MAX_PSK_KEY_LEN]; + word32 outSz = 0; + ProtocolVersion pv; + + pv.major = SSLv3_MAJOR; + pv.minor = TLSv1_3_MINOR; + + /* ImportedIdentity serialization matches the independent vector. */ + ExpectIntEQ(TLSX_PreSharedKey_CreateImportedIdentity(test_kat_id, + (word16)XSTRLEN((const char*)test_kat_id), ctx, ctxLen, targetKdfMac, + pv, ii, &iiSz), 0); + ExpectIntEQ((int)iiSz, (int)expIISz); + ExpectIntEQ(XMEMCMP(ii, expII, expIISz), 0); + + /* Derived imported PSK (ipskx) matches the independent vector. */ + ExpectIntEQ(DeriveImportedPsk(test_kat_epsk, (word32)sizeof(test_kat_epsk), + 0, expII, expIISz, importerHash, targetKdfMac, TLSv1_3_MINOR, 0, out, + &outSz, NULL, INVALID_DEVID), 0); + ExpectIntEQ((int)outSz, (int)expIpskSz); + ExpectIntEQ(XMEMCMP(out, expIpsk, expIpskSz), 0); + + return EXPECT_RESULT(); +} + +static int test_tls13_external_psk_importer_kat(void) +{ + EXPECT_DECLS; + + ExpectIntEQ(test_tls13_psk_importer_kat_one(NULL, 0, sha256_mac, WC_SHA256, + test_kat_ii1, (word16)sizeof(test_kat_ii1), + test_kat_ipsk1, (word32)sizeof(test_kat_ipsk1)), TEST_SUCCESS); +#if defined(WOLFSSL_SHA384) + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_ctx, + (word16)(sizeof(test_kat_ctx) - 1), sha384_mac, WC_SHA256, + test_kat_ii2, (word16)sizeof(test_kat_ii2), + test_kat_ipsk2, (word32)sizeof(test_kat_ipsk2)), TEST_SUCCESS); + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_ctx, + (word16)(sizeof(test_kat_ctx) - 1), sha256_mac, WC_SHA384, + test_kat_ii3, (word16)sizeof(test_kat_ii3), + test_kat_ipsk3, (word32)sizeof(test_kat_ipsk3)), TEST_SUCCESS); +#endif + + return EXPECT_RESULT(); +} #else static int test_tls13_external_psk_importer(void) { @@ -30663,6 +30770,10 @@ static int test_tls13_external_psk_importer_parse(void) { return TEST_SKIPPED; } +static int test_tls13_external_psk_importer_kat(void) +{ + return TEST_SKIPPED; +} #endif #if defined(WOLFSSL_TLS13) && defined(OPENSSL_EXTRA) && \ @@ -35798,6 +35909,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_prioritize_psk), TEST_DECL(test_tls13_external_psk_importer), TEST_DECL(test_tls13_external_psk_importer_parse), + TEST_DECL(test_tls13_external_psk_importer_kat), /* Can't memory test as client/server hangs. */ TEST_DECL(test_wc_CryptoCb), diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 21a1e997106..1cf1091e259 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3873,6 +3873,11 @@ WOLFSSL_LOCAL int TLSX_PreSharedKey_CreateImportedIdentity(const byte* id, WOLFSSL_LOCAL int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, word16 length, byte** id, word16* id_len, byte** ctx,word16* ctx_len, byte* hkdf, ProtocolVersion* protocol); +WOLFSSL_LOCAL int DeriveImportedPsk(const byte* epsk, word32 epskSz, + int preExtracted, const byte* importedIdentity, + word32 importedIdentitySz, int importerHash, byte targetKdfMac, + byte protocolMinor, int isDtls, byte* out, word32* outSz, + void* heap, int devId); #endif #endif /* HAVE_SESSION_TICKET || !NO_PSK */ diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 77b81251e28..64b3b749b40 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -3283,10 +3283,13 @@ enum { /* ssl Constants */ #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER /* On entry each *Sz holds the buffer capacity; on exit the actual length. - * identity, context and key are opaque byte buffers (RFC 9258). + * identity, context and key are opaque byte buffers (RFC 9258); identity is + * capped at MAX_PSK_ID_LEN and context at MAX_PSK_CTX_LEN. * hashAlgo is the hash function associated with the external PSK: it is * pre-initialized to WC_SHA256 (the RFC 9258 default) and only needs to be - * changed if the EPSK is associated with a different hash. */ + * changed if the EPSK is associated with a different hash. + * The callback must be deterministic: it is invoked more than once per + * connection and must return the same values each time. */ typedef int (*wc_psk_client_importer_callback)(WOLFSSL* ssl, unsigned char* identity, word32* identitySz, unsigned char* context, word32* contextSz, @@ -3323,7 +3326,9 @@ enum { /* ssl Constants */ * on entry *keySz holds the key buffer capacity, on exit the actual key * length (RFC 9258). hashAlgo is the hash associated with the external PSK, * pre-initialized to WC_SHA256; change it only for an EPSK associated with - * a different hash. */ + * a different hash. The callback must be deterministic: it may be invoked + * more than once per connection and must return the same values each + * time. */ typedef int (*wc_psk_server_importer_callback)(WOLFSSL* ssl, const unsigned char* identity, word32 identitySz, const unsigned char* context, word32 contextSz, From 2de0c7787386f18a06e6fa4287b20880f3e91524 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 20:04:35 +0000 Subject: [PATCH 07/10] RFC 9258 importer: remove pre-extracted EPSK option Drop wolfSSL_external_psk_pre_extracted() and the associated psk_externalKeyPreExtracted state and derivation branch. It was a non-standard convenience not required by RFC 9258 (the import always performs epskx = HKDF-Extract(0, epsk)) and is not wanted for upstreaming. * Remove the public API and its prototype/documentation. * Remove the Arrays.psk_externalKeyPreExtracted bitfield. * DeriveImportedPsk()/DeriveImportedPreSharedKey() always HKDF-Extract. * Drop the pre-extracted handshake test case. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- doc/dox_comments/header_files/ssl.h | 31 ------------ src/tls13.c | 75 ++++++++--------------------- tests/api.c | 22 +++------ wolfssl/internal.h | 10 ++-- wolfssl/ssl.h | 4 -- 5 files changed, 30 insertions(+), 112 deletions(-) diff --git a/doc/dox_comments/header_files/ssl.h b/doc/dox_comments/header_files/ssl.h index a2cecd00851..1913bf9a1c5 100644 --- a/doc/dox_comments/header_files/ssl.h +++ b/doc/dox_comments/header_files/ssl.h @@ -15251,7 +15251,6 @@ void wolfSSL_set_psk_server_tls13_callback(WOLFSSL* ssl, \sa wolfSSL_set_psk_client_importer_callback \sa wolfSSL_CTX_set_psk_server_importer_callback - \sa wolfSSL_external_psk_pre_extracted */ void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, wc_psk_client_importer_callback cb); @@ -15276,7 +15275,6 @@ void wolfSSL_CTX_set_psk_client_importer_callback(WOLFSSL_CTX* ctx, \sa wolfSSL_CTX_set_psk_client_importer_callback \sa wolfSSL_set_psk_server_importer_callback - \sa wolfSSL_external_psk_pre_extracted */ void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, wc_psk_client_importer_callback cb); @@ -15307,7 +15305,6 @@ void wolfSSL_set_psk_client_importer_callback(WOLFSSL* ssl, \sa wolfSSL_set_psk_server_importer_callback \sa wolfSSL_CTX_set_psk_client_importer_callback - \sa wolfSSL_external_psk_pre_extracted */ void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, wc_psk_server_importer_callback cb); @@ -15332,38 +15329,10 @@ void wolfSSL_CTX_set_psk_server_importer_callback(WOLFSSL_CTX* ctx, \sa wolfSSL_CTX_set_psk_server_importer_callback \sa wolfSSL_set_psk_client_importer_callback - \sa wolfSSL_external_psk_pre_extracted */ void wolfSSL_set_psk_server_importer_callback(WOLFSSL* ssl, wc_psk_server_importer_callback cb); -/*! - \ingroup Setup - - \brief This function marks whether the external PSK supplied by the importer - callback is already a pre-extracted pseudorandom key. When enabled, the - imported-PSK derivation skips the HKDF-Extract step (RFC 9258, Section 3.1). - Both peers must agree on this setting. Requires WOLFSSL_EXTERNAL_PSK_IMPORTER. - - \return 0 on success. - \return BAD_FUNC_ARG when ssl is NULL. - - \param [in,out] ssl a pointer to a WOLFSSL structure, created using - wolfSSL_new(). - \param [in] opt non-zero to treat the external PSK as pre-extracted. - - _Example_ - \code - WOLFSSL* ssl; - ... - wolfSSL_external_psk_pre_extracted(ssl, 1); - \endcode - - \sa wolfSSL_set_psk_client_importer_callback - \sa wolfSSL_set_psk_server_importer_callback -*/ -int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt); - /*! \ingroup Setup diff --git a/src/tls13.c b/src/tls13.c index cad25f05153..4713b64e5b8 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -1213,8 +1213,7 @@ static const byte derivedPskLabel[DERIVED_PSK_LABEL_SZ + 1] = * (RFC 9258, Section 3.1). This core routine is independent of WOLFSSL state so * it can be exercised directly with known-answer test vectors. * - * epsk/epskSz External PSK base key (or a pre-extracted PRK). - * preExtracted Non-zero if epsk is already a PRK (skip HKDF-Extract). + * epsk/epskSz External PSK base key. * importedIdentity Serialized ImportedIdentity (hashed as the context). * importedIdentitySz Length of importedIdentity. * importerHash Hash associated with the EPSK (e.g. WC_SHA256), used for @@ -1231,10 +1230,9 @@ static const byte derivedPskLabel[DERIVED_PSK_LABEL_SZ + 1] = * Returns 0 on success, otherwise a negative error. */ WOLFSSL_LOCAL int DeriveImportedPsk(const byte* epsk, word32 epskSz, - int preExtracted, const byte* importedIdentity, - word32 importedIdentitySz, int importerHash, byte targetKdfMac, - byte protocolMinor, int isDtls, byte* out, word32* outSz, - void* heap, int devId) + const byte* importedIdentity, word32 importedIdentitySz, + int importerHash, byte targetKdfMac, byte protocolMinor, int isDtls, + byte* out, word32* outSz, void* heap, int devId) { int ret; const byte* protocol; @@ -1357,28 +1355,19 @@ WOLFSSL_LOCAL int DeriveImportedPsk(const byte* epsk, word32 epskSz, /* okm holds ipskx; prk holds epskx. Both are dedicated buffers so the * HKDF input may safely alias the output buffer. */ PRIVATE_KEY_UNLOCK(); - if (preExtracted) { - /* The external PSK is already a pseudorandom key (the result of an - * earlier HKDF-Extract), so derive ipskx directly with an - * HKDF-Expand-Label. */ - ret = wc_HKDF_Expand_ex(importerHash, epsk, epskSz, hkdfLabel, idx, - okm, outputLen, heap, devId); - } - else { - /* epskx = HKDF-Extract(0, epsk) */ - #if !defined(HAVE_FIPS) || \ - (defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)) - ret = wc_HKDF_Extract_ex(importerHash, NULL, 0, epsk, epskSz, prk, - heap, devId); - #else - ret = wc_HKDF_Extract(importerHash, NULL, 0, epsk, epskSz, prk); - #endif - if (ret == 0) { - /* ipskx = HKDF-Expand-Label(epskx, "derived psk", - * Hash(ImportedIdentity), L) */ - ret = wc_HKDF_Expand_ex(importerHash, prk, (word32)hashSz, - hkdfLabel, idx, okm, outputLen, heap, devId); - } + /* epskx = HKDF-Extract(0, epsk) */ + #if !defined(HAVE_FIPS) || \ + (defined(FIPS_VERSION_GE) && FIPS_VERSION_GE(6,0)) + ret = wc_HKDF_Extract_ex(importerHash, NULL, 0, epsk, epskSz, prk, + heap, devId); + #else + ret = wc_HKDF_Extract(importerHash, NULL, 0, epsk, epskSz, prk); + #endif + if (ret == 0) { + /* ipskx = HKDF-Expand-Label(epskx, "derived psk", + * Hash(ImportedIdentity), L) */ + ret = wc_HKDF_Expand_ex(importerHash, prk, (word32)hashSz, + hkdfLabel, idx, okm, outputLen, heap, devId); } PRIVATE_KEY_LOCK(); @@ -1424,10 +1413,9 @@ static int DeriveImportedPreSharedKey(WOLFSSL* ssl, PreSharedKey* psk, keySz = ssl->arrays->psk_keySz; ret = DeriveImportedPsk(ssl->arrays->psk_key, ssl->arrays->psk_keySz, - ssl->arrays->psk_externalKeyPreExtracted, psk->identity, - psk->identityLen, importerHash, psk->hmac, ssl->version.minor, - ssl->options.dtls, ssl->arrays->psk_key, &keySz, ssl->heap, - ssl->devId); + psk->identity, psk->identityLen, importerHash, psk->hmac, + ssl->version.minor, ssl->options.dtls, ssl->arrays->psk_key, &keySz, + ssl->heap, ssl->devId); if (ret == 0) ssl->arrays->psk_keySz = keySz; @@ -15833,29 +15821,6 @@ const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash) } return name; } - -#if defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) -/* Mark whether the external PSK provided by the importer callback is already a - * pre-extracted pseudorandom key. When set, the imported-PSK derivation skips - * the HKDF-Extract step (RFC 9258, Section 3.1). - * - * ssl The SSL/TLS object. - * opt Non-zero to treat the external PSK as pre-extracted. - * returns BAD_FUNC_ARG when ssl is NULL and 0 on success. - */ -int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt) -{ - if (ssl == NULL) - return BAD_FUNC_ARG; - - if (opt) - ssl->arrays->psk_externalKeyPreExtracted = 1; - else - ssl->arrays->psk_externalKeyPreExtracted = 0; - - return 0; -} -#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ #endif /* !NO_PSK */ diff --git a/tests/api.c b/tests/api.c index 080275fed83..0a9bf636a30 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30496,7 +30496,7 @@ static int test_psk_importer_server_cb(WOLFSSL* ssl, const unsigned char* id, /* Run a single TLS 1.3 handshake that authenticates with an imported external * PSK (RFC 9258), restricted to the given cipher suite. */ static int test_tls13_external_psk_importer_one(const char* cipher, - int expectedSuite, int useContext, int preExtracted, int expectFail) + int expectedSuite, int useContext, int expectFail) { EXPECT_DECLS; WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; @@ -30519,11 +30519,6 @@ static int test_tls13_external_psk_importer_one(const char* cipher, wolfSSL_set_psk_server_importer_callback(ssl_s, test_psk_importer_server_cb); - if (preExtracted) { - ExpectIntEQ(wolfSSL_external_psk_pre_extracted(ssl_c, 1), 0); - ExpectIntEQ(wolfSSL_external_psk_pre_extracted(ssl_s, 1), 0); - } - /* Restrict both sides to the suite under test (importer callback setup * rebuilds the suite list, so set the cipher list afterwards). */ ExpectIntEQ(wolfSSL_set_cipher_list(ssl_c, cipher), WOLFSSL_SUCCESS); @@ -30556,16 +30551,13 @@ static int test_tls13_external_psk_importer(void) /* HKDF_SHA256 target_kdf, without and with an optional context. */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 0, 0, 0), TEST_SUCCESS); - ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 0, 0), TEST_SUCCESS); - /* Pre-extracted EPSK (HKDF-Extract is skipped during import). */ + 0x1301, 0, 0), TEST_SUCCESS); ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 1, 0), TEST_SUCCESS); + 0x1301, 1, 0), TEST_SUCCESS); #if defined(WOLFSSL_SHA384) && defined(WOLFSSL_AES_256) /* HKDF_SHA384 target_kdf exercises the L = 48 derived-PSK length. */ ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES256-GCM-SHA384", - 0x1302, 1, 0, 0), TEST_SUCCESS); + 0x1302, 1, 0), TEST_SUCCESS); #endif #if defined(WOLFSSL_SHA384) @@ -30574,7 +30566,7 @@ static int test_tls13_external_psk_importer(void) * target KDF (RFC 9258 Section 3.1). */ test_psk_importer_hash = WC_SHA384; ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 0, 0), TEST_SUCCESS); + 0x1301, 1, 0), TEST_SUCCESS); test_psk_importer_hash = WC_SHA256; #endif @@ -30582,7 +30574,7 @@ static int test_tls13_external_psk_importer(void) * -> handshake must fail. */ test_psk_importer_server_wrong_key = 1; ExpectIntEQ(test_tls13_external_psk_importer_one("TLS13-AES128-GCM-SHA256", - 0x1301, 1, 0, 1), TEST_SUCCESS); + 0x1301, 1, 1), TEST_SUCCESS); test_psk_importer_server_wrong_key = 0; return EXPECT_RESULT(); @@ -30733,7 +30725,7 @@ static int test_tls13_psk_importer_kat_one(const byte* ctx, word16 ctxLen, /* Derived imported PSK (ipskx) matches the independent vector. */ ExpectIntEQ(DeriveImportedPsk(test_kat_epsk, (word32)sizeof(test_kat_epsk), - 0, expII, expIISz, importerHash, targetKdfMac, TLSv1_3_MINOR, 0, out, + expII, expIISz, importerHash, targetKdfMac, TLSv1_3_MINOR, 0, out, &outSz, NULL, INVALID_DEVID), 0); ExpectIntEQ((int)outSz, (int)expIpskSz); ExpectIntEQ(XMEMCMP(out, expIpsk, expIpskSz), 0); diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 1cf1091e259..00e7725738a 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3874,10 +3874,9 @@ WOLFSSL_LOCAL int TLSX_PreSharedKey_ParseImportedIdentity(byte* input, word16 length, byte** id, word16* id_len, byte** ctx,word16* ctx_len, byte* hkdf, ProtocolVersion* protocol); WOLFSSL_LOCAL int DeriveImportedPsk(const byte* epsk, word32 epskSz, - int preExtracted, const byte* importedIdentity, - word32 importedIdentitySz, int importerHash, byte targetKdfMac, - byte protocolMinor, int isDtls, byte* out, word32* outSz, - void* heap, int devId); + const byte* importedIdentity, word32 importedIdentitySz, + int importerHash, byte targetKdfMac, byte protocolMinor, int isDtls, + byte* out, word32* outSz, void* heap, int devId); #endif #endif /* HAVE_SESSION_TICKET || !NO_PSK */ @@ -5417,9 +5416,6 @@ typedef struct Arrays { char client_identity[MAX_PSK_ID_LEN + NULL_TERM_LEN]; char server_hint[MAX_PSK_ID_LEN + NULL_TERM_LEN]; byte psk_key[MAX_PSK_KEY_LEN]; -#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) - byte psk_externalKeyPreExtracted:1; -#endif /* WOLFSSL_TLS13 && WOLFSSL_EXTERNAL_PSK_IMPORTER */ #endif /* HAVE_SESSION_TICKET || !NO_PSK */ byte clientRandom[RAN_LEN]; #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 64b3b749b40..3259ddb56b0 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -3355,10 +3355,6 @@ enum { /* ssl Constants */ #ifdef WOLFSSL_TLS13 WOLFSSL_API const char* wolfSSL_get_cipher_name_by_hash(WOLFSSL* ssl, const char* hash); - - #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER - WOLFSSL_API int wolfSSL_external_psk_pre_extracted(WOLFSSL* ssl, int opt); - #endif #endif #endif /* NO_PSK */ From c27d995e1ba5f24ec2715eb4d8014cc7b4bf8fbb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 20:15:22 +0000 Subject: [PATCH 08/10] RFC 9258 importer: add GnuTLS cross-checked KAT vector Add a fourth known-answer vector to the importer KAT using the exact external_identity (0xCAFECAFE), context (0xDEADBEEF) and expected ImportedIdentity from GnuTLS's own test (tests/psk-importer.c, GnuTLS >= 3.8.1). This confirms our ImportedIdentity serialization matches an independent RFC 9258 implementation. The ipskx expectation is computed independently. The KAT helper now takes the identity and EPSK as parameters so vectors with different inputs can be exercised. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- tests/api.c | 54 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/tests/api.c b/tests/api.c index 0a9bf636a30..b75bab8712b 100644 --- a/tests/api.c +++ b/tests/api.c @@ -30702,7 +30702,26 @@ static const byte test_kat_ipsk3[] = { }; #endif /* WOLFSSL_SHA384 */ -static int test_tls13_psk_importer_kat_one(const byte* ctx, word16 ctxLen, +/* V4: cross-implementation vector. The external_identity (0xCAFECAFE), context + * (0xDEADBEEF) and the expected ImportedIdentity below are exactly those used + * by GnuTLS's own test, tests/psk-importer.c (GnuTLS >= 3.8.1), confirming our + * ImportedIdentity serialization matches an independent RFC 9258 + * implementation. The ipskx value is the independently computed expectation. */ +static const byte test_kat_gnutls_id[] = { 0xca, 0xfe, 0xca, 0xfe }; +static const byte test_kat_gnutls_ctx[] = { 0xde, 0xad, 0xbe, 0xef }; +static const byte test_kat_gnutls_epsk[] = { 0xde, 0xad, 0xbe, 0xef }; +static const byte test_kat_gnutls_ii[] = { + 0x00, 0x04, 0xca, 0xfe, 0xca, 0xfe, 0x00, 0x04, 0xde, 0xad, 0xbe, 0xef, + 0x03, 0x04, 0x00, 0x01 +}; +static const byte test_kat_gnutls_ipsk[] = { + 0x77, 0x49, 0xf3, 0x1b, 0x70, 0xcf, 0xec, 0x8f, 0x97, 0x83, 0x41, 0x3f, + 0x71, 0xe7, 0x15, 0xfa, 0x61, 0x73, 0xa9, 0xa5, 0x20, 0xd6, 0x0b, 0x85, + 0x06, 0x6d, 0xe2, 0x92, 0x22, 0xb3, 0x79, 0xff +}; + +static int test_tls13_psk_importer_kat_one(const byte* id, word16 idLen, + const byte* ctx, word16 ctxLen, const byte* epsk, word32 epskSz, byte targetKdfMac, int importerHash, const byte* expII, word16 expIISz, const byte* expIpsk, word32 expIpskSz) { @@ -30717,16 +30736,14 @@ static int test_tls13_psk_importer_kat_one(const byte* ctx, word16 ctxLen, pv.minor = TLSv1_3_MINOR; /* ImportedIdentity serialization matches the independent vector. */ - ExpectIntEQ(TLSX_PreSharedKey_CreateImportedIdentity(test_kat_id, - (word16)XSTRLEN((const char*)test_kat_id), ctx, ctxLen, targetKdfMac, - pv, ii, &iiSz), 0); + ExpectIntEQ(TLSX_PreSharedKey_CreateImportedIdentity(id, idLen, ctx, ctxLen, + targetKdfMac, pv, ii, &iiSz), 0); ExpectIntEQ((int)iiSz, (int)expIISz); ExpectIntEQ(XMEMCMP(ii, expII, expIISz), 0); /* Derived imported PSK (ipskx) matches the independent vector. */ - ExpectIntEQ(DeriveImportedPsk(test_kat_epsk, (word32)sizeof(test_kat_epsk), - expII, expIISz, importerHash, targetKdfMac, TLSv1_3_MINOR, 0, out, - &outSz, NULL, INVALID_DEVID), 0); + ExpectIntEQ(DeriveImportedPsk(epsk, epskSz, expII, expIISz, importerHash, + targetKdfMac, TLSv1_3_MINOR, 0, out, &outSz, NULL, INVALID_DEVID), 0); ExpectIntEQ((int)outSz, (int)expIpskSz); ExpectIntEQ(XMEMCMP(out, expIpsk, expIpskSz), 0); @@ -30736,21 +30753,34 @@ static int test_tls13_psk_importer_kat_one(const byte* ctx, word16 ctxLen, static int test_tls13_external_psk_importer_kat(void) { EXPECT_DECLS; + word16 idLen = (word16)XSTRLEN((const char*)test_kat_id); - ExpectIntEQ(test_tls13_psk_importer_kat_one(NULL, 0, sha256_mac, WC_SHA256, + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_id, idLen, NULL, 0, + test_kat_epsk, (word32)sizeof(test_kat_epsk), sha256_mac, WC_SHA256, test_kat_ii1, (word16)sizeof(test_kat_ii1), test_kat_ipsk1, (word32)sizeof(test_kat_ipsk1)), TEST_SUCCESS); #if defined(WOLFSSL_SHA384) - ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_ctx, - (word16)(sizeof(test_kat_ctx) - 1), sha384_mac, WC_SHA256, + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_id, idLen, + test_kat_ctx, (word16)(sizeof(test_kat_ctx) - 1), + test_kat_epsk, (word32)sizeof(test_kat_epsk), sha384_mac, WC_SHA256, test_kat_ii2, (word16)sizeof(test_kat_ii2), test_kat_ipsk2, (word32)sizeof(test_kat_ipsk2)), TEST_SUCCESS); - ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_ctx, - (word16)(sizeof(test_kat_ctx) - 1), sha256_mac, WC_SHA384, + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_id, idLen, + test_kat_ctx, (word16)(sizeof(test_kat_ctx) - 1), + test_kat_epsk, (word32)sizeof(test_kat_epsk), sha256_mac, WC_SHA384, test_kat_ii3, (word16)sizeof(test_kat_ii3), test_kat_ipsk3, (word32)sizeof(test_kat_ipsk3)), TEST_SUCCESS); #endif + /* Cross-checked against GnuTLS tests/psk-importer.c. */ + ExpectIntEQ(test_tls13_psk_importer_kat_one(test_kat_gnutls_id, + (word16)sizeof(test_kat_gnutls_id), test_kat_gnutls_ctx, + (word16)sizeof(test_kat_gnutls_ctx), test_kat_gnutls_epsk, + (word32)sizeof(test_kat_gnutls_epsk), sha256_mac, WC_SHA256, + test_kat_gnutls_ii, (word16)sizeof(test_kat_gnutls_ii), + test_kat_gnutls_ipsk, (word32)sizeof(test_kat_gnutls_ipsk)), + TEST_SUCCESS); + return EXPECT_RESULT(); } #else From 20b768c937eac455a02903c1b20e5fdf848eed10 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 20:25:15 +0000 Subject: [PATCH 09/10] examples: add RFC 9258 external PSK importer mode Wire the importer into the example client and server so they can be used for interoperability testing (e.g. against GnuTLS >= 3.8.1). * Add example importer callbacks (my_psk_client_importer_cb / my_psk_server_importer_cb) in wolfssl/test.h: same external identity (kIdentityStr), no context, default SHA-256 importer hash, and a fixed 32-byte external PSK on both sides. * Add a --psk-importer option to examples/client and examples/server that selects the importer callbacks (TLS 1.3 only). Gated behind WOLFSSL_EXTERNAL_PSK_IMPORTER; the regular PSK paths are unchanged when the option/feature is absent. * Document the option in the usage output of both apps. Verified client<->server interop over a socket for the SHA-256 and SHA-384 target KDFs. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- examples/client/client.c | 39 ++++++++++++++++++++++- examples/server/server.c | 39 ++++++++++++++++++++++- wolfssl/test.h | 69 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/examples/client/client.c b/examples/client/client.c index 64958f2091e..23dae8a9808 100644 --- a/examples/client/client.c +++ b/examples/client/client.c @@ -1466,6 +1466,10 @@ static const char* client_usage_msg[][81] = { !defined(NO_PSK) "--psk-with-certs Use TLS 1.3 PSK with certificates\n", /* 74 */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + "--psk-importer Import an external PSK per RFC 9258\n", +#endif #ifdef HAVE_RPK "--rpk Use RPK for the defined certificates\n", /* 75 */ #endif @@ -1736,6 +1740,10 @@ static const char* client_usage_msg[][81] = { !defined(NO_PSK) "--psk-with-certs Use TLS 1.3 PSK with certificates\n", /* 74 */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + "--psk-importer Import an external PSK per RFC 9258\n", +#endif #ifdef HAVE_RPK "--rpk Use RPK for the defined certificates\n", /* 75 */ #endif @@ -1989,6 +1997,10 @@ static void Usage(void) !defined(NO_PSK) printf("%s", msg[++msgid]); /* --psk-with-certs */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + printf("%s", msg[++msgid]); /* --psk-importer */ +#endif #ifdef HAVE_RPK printf("%s", msg[++msgid]); /* --rpk */ #endif @@ -2192,6 +2204,10 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) #if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_WITH_EXTERN_PSK) && \ !defined(NO_PSK) { "psk-with-certs", 0, 272 }, +#endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + { "psk-importer", 0, 273 }, #endif { 0, 0, 0 } }; @@ -2201,6 +2217,7 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) int usePsk = 0; int opensslPsk = 0; int usePskWithCerts = 0; + int usePskImporter = 0; int useAnon = 0; int sendGET = 0; int benchmark = 0; @@ -2441,6 +2458,7 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) (void)opensslPsk; (void)fileFormat; (void)usePskWithCerts; + (void)usePskImporter; StackTrap(); /* Reinitialize the global myVerifyAction. */ @@ -3103,6 +3121,15 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) break; #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + case 273: + /* Import an external PSK per RFC 9258. */ + usePskImporter = 1; + usePsk = 1; + break; +#endif + default: Usage(); XEXIT_T(MY_EX_USAGE); @@ -3518,6 +3545,15 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) #ifndef NO_PSK const char *defaultCipherList = cipherList; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) + if (usePskImporter) { + /* RFC 9258 external PSK importer (TLS 1.3 only). */ + wolfSSL_CTX_set_psk_client_importer_callback(ctx, + my_psk_client_importer_cb); + } + else +#endif + { wolfSSL_CTX_set_psk_client_callback(ctx, my_psk_client_cb); #ifdef WOLFSSL_TLS13 #if !defined(WOLFSSL_PSK_TLS13_CB) && !defined(WOLFSSL_PSK_ONE_ID) @@ -3538,7 +3574,8 @@ THREAD_RETURN WOLFSSL_THREAD client_test(void* args) } } #endif -#endif +#endif /* WOLFSSL_TLS13 */ + } /* end of non-importer PSK setup */ if (defaultCipherList == NULL) { #if defined(HAVE_AESGCM) && !defined(NO_DH) #ifdef WOLFSSL_TLS13 diff --git a/examples/server/server.c b/examples/server/server.c index 2ac169dce57..3f23da1bcdd 100644 --- a/examples/server/server.c +++ b/examples/server/server.c @@ -1132,6 +1132,10 @@ static const char* server_usage_msg[][71] = { !defined(NO_PSK) "--psk-with-certs Use TLS 1.3 PSK with certificates\n", /* 65 */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + "--psk-importer Import an external PSK per RFC 9258\n", +#endif #ifdef WOLFSSL_DUAL_ALG_CERTS "--altPrivKey Generate alternative signature with this key.\n", /* 66 */ #endif @@ -1358,6 +1362,10 @@ static const char* server_usage_msg[][71] = { !defined(NO_PSK) "--psk-with-certs Use TLS 1.3 PSK with certificates\n", /* 65 */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + "--psk-importer Import an external PSK per RFC 9258\n", +#endif #ifdef WOLFSSL_DUAL_ALG_CERTS "--altPrivKey Generate alternative signature with this key.\n", /* 66 */ #endif @@ -1537,6 +1545,10 @@ static void Usage(void) !defined(NO_PSK) printf("%s", msg[++msgId]); /* --psk-with-certs */ #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + printf("%s", msg[++msgId]); /* --psk-importer */ +#endif #ifdef WOLFSSL_DUAL_ALG_CERTS printf("%s", msg[++msgId]); /* --altPrivKey */ #endif @@ -1673,6 +1685,10 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) #if defined(WOLFSSL_TLS13) && defined(WOLFSSL_CERT_WITH_EXTERN_PSK) && \ !defined(NO_PSK) { "psk-with-certs", 0, 271 }, +#endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + { "psk-importer", 0, 272 }, #endif { 0, 0, 0 } }; @@ -1691,6 +1707,7 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) int usePsk = 0; int usePskPlus = 0; int usePskWithCerts = 0; + int usePskImporter = 0; int useAnon = 0; int doDTLS = 0; int dtlsUDP = 0; @@ -1928,6 +1945,7 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) (void)usePqc; (void)altPrivKey; (void)usePskWithCerts; + (void)usePskImporter; #ifdef WOLFSSL_TIRTOS fdOpenSession(Task_self()); @@ -2625,6 +2643,15 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) break; #endif +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) && \ + !defined(NO_PSK) + case 272: + /* Import an external PSK per RFC 9258. */ + usePskImporter = 1; + usePsk = 1; + break; +#endif + case -1: default: Usage(); @@ -3057,6 +3084,15 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) #ifndef NO_PSK const char *defaultCipherList = cipherList; +#if defined(WOLFSSL_TLS13) && defined(WOLFSSL_EXTERNAL_PSK_IMPORTER) + if (usePskImporter) { + /* RFC 9258 external PSK importer (TLS 1.3 only). */ + wolfSSL_CTX_set_psk_server_importer_callback(ctx, + my_psk_server_importer_cb); + } + else +#endif + { wolfSSL_CTX_set_psk_server_callback(ctx, my_psk_server_cb); #ifdef WOLFSSL_TLS13 wolfSSL_CTX_set_psk_server_tls13_callback(ctx, my_psk_server_tls13_cb); @@ -3068,7 +3104,8 @@ THREAD_RETURN WOLFSSL_THREAD server_test(void* args) } } #endif - #endif + #endif /* WOLFSSL_TLS13 */ + } /* end of non-importer PSK setup */ if (sendPskIdentityHint == 1) wolfSSL_CTX_use_psk_identity_hint(ctx, "cyassl server"); diff --git a/wolfssl/test.h b/wolfssl/test.h index 97454e1702b..940a3a15b6c 100644 --- a/wolfssl/test.h +++ b/wolfssl/test.h @@ -2099,6 +2099,75 @@ static WC_INLINE unsigned int my_psk_server_tls13_cb(WOLFSSL* ssl, return ret; } + +#ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER +/* Example RFC 9258 external PSK importer callbacks. Both sides use the same + * external identity (kIdentityStr), no context, the default SHA-256 importer + * hash, and the fixed 32-byte external PSK below. Configure an interop peer + * (e.g. GnuTLS >= 3.8.1) with matching values. */ +static WC_INLINE void my_psk_importer_fill_key(unsigned char* key) +{ + int i; + int b = 0x01; + for (i = 0; i < 32; i++, b += 0x22) { + if (b >= 0x100) + b = 0x01; + key[i] = (unsigned char)b; + } +} + +static WC_INLINE int my_psk_client_importer_cb(WOLFSSL* ssl, + unsigned char* identity, word32* identitySz, unsigned char* context, + word32* contextSz, unsigned char* key, word32* keySz, int* hashAlgo) +{ + word32 idLen = (word32)XSTRLEN(kIdentityStr); + + (void)ssl; + (void)hashAlgo; /* leave the default WC_SHA256 importer hash */ + + if (idLen > *identitySz) + return -1; + XMEMCPY(identity, kIdentityStr, idLen); + *identitySz = idLen; + + /* No optional context in this example. */ + if (context != NULL && contextSz != NULL) + *contextSz = 0; + + if (32 > *keySz) + return -1; + my_psk_importer_fill_key(key); + *keySz = 32; + + return 0; +} + +static WC_INLINE int my_psk_server_importer_cb(WOLFSSL* ssl, + const unsigned char* identity, word32 identitySz, + const unsigned char* context, word32 contextSz, unsigned char* key, + word32* keySz, int* hashAlgo) +{ + word32 idLen = (word32)XSTRLEN(kIdentityStr); + + (void)ssl; + (void)context; + (void)hashAlgo; /* leave the default WC_SHA256 importer hash */ + + if (identity == NULL || identitySz != idLen || + XMEMCMP(identity, kIdentityStr, idLen) != 0) + return -1; + /* This example advertises no context. */ + if (contextSz != 0) + return -1; + + if (32 > *keySz) + return -1; + my_psk_importer_fill_key(key); + *keySz = 32; + + return 0; +} +#endif /* WOLFSSL_EXTERNAL_PSK_IMPORTER */ #endif #ifdef OPENSSL_EXTRA From fe4da112a9eb224b9bc23c57aa85ee60ff55efbb Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 22 Jun 2026 07:46:38 +0000 Subject: [PATCH 10/10] examples: use a non-empty importer context in the example callbacks Change the example RFC 9258 importer callbacks (wolfssl/test.h) to carry a non-empty optional context ("wolfSSL importer example context") instead of an empty one, to better illustrate the feature. The server callback now verifies the received context matches. Verified wolfSSL <-> GnuTLS 3.8.4 interop (both directions, TLS 1.3, TLS_AES_128_GCM_SHA256) still succeeds with the context present, and that a mismatched context is correctly rejected with a binder-verification failure. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01XRMrPoMWjro4shL6W2JHXw --- wolfssl/test.h | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/wolfssl/test.h b/wolfssl/test.h index 940a3a15b6c..d3b47fd5ce1 100644 --- a/wolfssl/test.h +++ b/wolfssl/test.h @@ -2102,9 +2102,11 @@ static WC_INLINE unsigned int my_psk_server_tls13_cb(WOLFSSL* ssl, #ifdef WOLFSSL_EXTERNAL_PSK_IMPORTER /* Example RFC 9258 external PSK importer callbacks. Both sides use the same - * external identity (kIdentityStr), no context, the default SHA-256 importer - * hash, and the fixed 32-byte external PSK below. Configure an interop peer - * (e.g. GnuTLS >= 3.8.1) with matching values. */ + * external identity (kIdentityStr), an example context string, the default + * SHA-256 importer hash, and the fixed 32-byte external PSK below. Configure an + * interop peer (e.g. GnuTLS >= 3.8.1) with matching values. */ +static const char* kImporterContextStr = "wolfSSL importer example context"; + static WC_INLINE void my_psk_importer_fill_key(unsigned char* key) { int i; @@ -2121,6 +2123,7 @@ static WC_INLINE int my_psk_client_importer_cb(WOLFSSL* ssl, word32* contextSz, unsigned char* key, word32* keySz, int* hashAlgo) { word32 idLen = (word32)XSTRLEN(kIdentityStr); + word32 ctxLen = (word32)XSTRLEN(kImporterContextStr); (void)ssl; (void)hashAlgo; /* leave the default WC_SHA256 importer hash */ @@ -2130,9 +2133,13 @@ static WC_INLINE int my_psk_client_importer_cb(WOLFSSL* ssl, XMEMCPY(identity, kIdentityStr, idLen); *identitySz = idLen; - /* No optional context in this example. */ - if (context != NULL && contextSz != NULL) - *contextSz = 0; + /* Provide an example (non-empty) optional context. */ + if (context != NULL && contextSz != NULL) { + if (ctxLen > *contextSz) + return -1; + XMEMCPY(context, kImporterContextStr, ctxLen); + *contextSz = ctxLen; + } if (32 > *keySz) return -1; @@ -2148,16 +2155,17 @@ static WC_INLINE int my_psk_server_importer_cb(WOLFSSL* ssl, word32* keySz, int* hashAlgo) { word32 idLen = (word32)XSTRLEN(kIdentityStr); + word32 ctxLen = (word32)XSTRLEN(kImporterContextStr); (void)ssl; - (void)context; (void)hashAlgo; /* leave the default WC_SHA256 importer hash */ if (identity == NULL || identitySz != idLen || XMEMCMP(identity, kIdentityStr, idLen) != 0) return -1; - /* This example advertises no context. */ - if (contextSz != 0) + /* Verify the example context advertised by the client. */ + if (contextSz != ctxLen || context == NULL || + XMEMCMP(context, kImporterContextStr, ctxLen) != 0) return -1; if (32 > *keySz)