Skip to content

Commit 5ade630

Browse files
committed
Enable support for mandatory PSKs
Add a new option to require that an external Pre-Shared Key is negotiated for a handshake to succeed, configured via the new APIs wolfSSL_CTX_require_psk()/wolfSSL_require_psk(). When set, a handshake that completes without negotiating an external PSK is aborted with PSK_MISSING_ERROR instead of falling back to a certificate handshake, so the PSK acts as an additional security factor. This is a TLS 1.3 / DTLS 1.3 feature. In (D)TLS 1.2 the use of a PSK is determined by the negotiated cipher suite, so a mandatory PSK is instead configured there by restricting the cipher suite list to PSK suites; the new APIs therefore reject non-TLS-1.3 contexts with BAD_FUNC_ARG. The requirement applies to external PSKs only (not session tickets): session-ticket resumption is exempt. To preserve forward secrecy a mandatory external PSK must also use an (EC)DHE key exchange; a pure psk_ke handshake is rejected with PSK_KEY_ERROR. When used with WOLFSSL_CERT_WITH_EXTERN_PSK, it also ensures that peers are properly authenticated with both the PSK and via certificates. The new APIs live alongside the existing wolfSSL_[CTX_]no_dhe_psk()/ only_dhe_psk() PSK options and do not depend on certificate support, so the feature is usable in NO_CERTS (PSK-only) builds. Added unit tests for the new APIs and enforcement.
1 parent 4de8190 commit 5ade630

9 files changed

Lines changed: 927 additions & 11 deletions

File tree

doc/dox_comments/header_files/ssl.h

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14134,6 +14134,71 @@ int wolfSSL_CTX_no_dhe_psk(WOLFSSL_CTX* ctx);
1413414134
*/
1413514135
int wolfSSL_no_dhe_psk(WOLFSSL* ssl);
1413614136

14137+
/*!
14138+
\ingroup Setup
14139+
14140+
\brief This function is called on a TLS v1.3 / DTLS v1.3 context to require
14141+
that an external Pre-Shared Key is negotiated for the handshake to succeed.
14142+
When set, a handshake that completes without negotiating an external PSK is
14143+
aborted with PSK_MISSING_ERROR instead of falling back to a certificate
14144+
handshake, so the PSK acts as an additional security factor. The requirement
14145+
keys off the external-PSK callback (it has no effect unless one is
14146+
registered) and session-ticket resumption is exempt. To preserve forward
14147+
secrecy a mandatory external PSK must also use an (EC)DHE key exchange; a
14148+
pure psk_ke handshake is rejected with PSK_KEY_ERROR. This applies to TLS 1.3
14149+
and DTLS 1.3 only; in (D)TLS 1.2 the use of a PSK is determined by the
14150+
negotiated cipher suite, so a mandatory PSK is instead configured by
14151+
restricting the cipher suite list to (preferably (EC)DHE-)PSK suites.
14152+
14153+
\param [in,out] ctx a pointer to a WOLFSSL_CTX structure, created using
14154+
wolfSSL_CTX_new().
14155+
14156+
\return BAD_FUNC_ARG if ctx is NULL or not at least TLS v1.3.
14157+
\return 0 if successful.
14158+
14159+
_Example_
14160+
\code
14161+
int ret;
14162+
WOLFSSL_CTX* ctx;
14163+
...
14164+
ret = wolfSSL_CTX_require_psk(ctx);
14165+
if (ret != 0) {
14166+
// failed to make a PSK mandatory
14167+
}
14168+
\endcode
14169+
14170+
\sa wolfSSL_require_psk
14171+
*/
14172+
int wolfSSL_CTX_require_psk(WOLFSSL_CTX* ctx);
14173+
14174+
/*!
14175+
\ingroup Setup
14176+
14177+
\brief This function is called on a TLS v1.3 / DTLS v1.3 wolfSSL object to
14178+
require that an external Pre-Shared Key is negotiated for the handshake to
14179+
succeed. See wolfSSL_CTX_require_psk() for the full behaviour.
14180+
14181+
\param [in,out] ssl a pointer to a WOLFSSL structure, created using
14182+
wolfSSL_new().
14183+
14184+
\return BAD_FUNC_ARG if ssl is NULL or not at least TLS v1.3.
14185+
\return 0 if successful.
14186+
14187+
_Example_
14188+
\code
14189+
int ret;
14190+
WOLFSSL* ssl;
14191+
...
14192+
ret = wolfSSL_require_psk(ssl);
14193+
if (ret != 0) {
14194+
// failed to make a PSK mandatory
14195+
}
14196+
\endcode
14197+
14198+
\sa wolfSSL_CTX_require_psk
14199+
*/
14200+
int wolfSSL_require_psk(WOLFSSL* ssl);
14201+
1413714202
/*!
1413814203
\ingroup IO
1413914204

src/internal.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7279,6 +7279,7 @@ int SetSSL_CTX(WOLFSSL* ssl, WOLFSSL_CTX* ctx, int writeDup)
72797279
ssl->options.verifyNone = ctx->verifyNone;
72807280
ssl->options.failNoCert = ctx->failNoCert;
72817281
ssl->options.failNoCertxPSK = ctx->failNoCertxPSK;
7282+
ssl->options.failNoPSK = ctx->failNoPSK;
72827283
ssl->options.sendVerify = ctx->sendVerify;
72837284

72847285
ssl->options.partialWrite = ctx->partialWrite;
@@ -27955,6 +27956,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e)
2795527956
case DUPE_ENTRY_E:
2795627957
return "duplicate entry error";
2795727958

27959+
case PSK_MISSING_ERROR:
27960+
return "psk missing error";
27961+
2795827962
case GETTIME_ERROR:
2795927963
return "gettimeofday() error";
2796027964

@@ -36210,6 +36214,7 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
3621036214
return missing_extension;
3621136215
case WC_NO_ERR_TRACE(MATCH_SUITE_ERROR):
3621236216
case WC_NO_ERR_TRACE(MISSING_HANDSHAKE_DATA):
36217+
case WC_NO_ERR_TRACE(PSK_MISSING_ERROR):
3621336218
return handshake_failure;
3621436219
case WC_NO_ERR_TRACE(VERSION_ERROR):
3621536220
return wolfssl_alert_protocol_version;

src/tls13.c

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4386,7 +4386,11 @@ static int SetupPskKey(WOLFSSL* ssl, PreSharedKey* psk, int clientHello)
43864386
return PSK_KEY_ERROR;
43874387
}
43884388
}
4389-
else if (ssl->options.onlyPskDheKe) {
4389+
else if (ssl->options.onlyPskDheKe ||
4390+
(ssl->options.failNoPSK && !psk->resumption)) {
4391+
/* A mandatory external PSK (failNoPSK) must be combined with
4392+
* (EC)DHE for forward secrecy, so reject a pure psk_ke
4393+
* negotiation. Session-ticket resumption is exempt. */
43904394
WOLFSSL_ERROR_VERBOSE(PSK_KEY_ERROR);
43914395
return PSK_KEY_ERROR;
43924396
}
@@ -5907,6 +5911,18 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
59075911
while (psk != NULL && !psk->chosen)
59085912
psk = psk->next;
59095913
if (psk == NULL) {
5914+
/* A mandatory PSK is satisfied by any PSK the server chose,
5915+
* including a resumption PSK - this matches the server-side
5916+
* failNoPSK semantics, where a negotiated PSK (external or
5917+
* resumption) is accepted. The error only fires when no PSK was
5918+
* chosen at all. havePSK is only set by an external-PSK callback,
5919+
* so a peer relying solely on session-ticket resumption is
5920+
* unaffected. */
5921+
if (ssl->options.havePSK && ssl->options.failNoPSK) {
5922+
WOLFSSL_MSG("Server did not negotiate a mandatory PSK");
5923+
WOLFSSL_ERROR_VERBOSE(PSK_MISSING_ERROR);
5924+
return PSK_MISSING_ERROR;
5925+
}
59105926
ssl->options.resuming = 0;
59115927
ssl->arrays->psk_keySz = 0;
59125928
XMEMSET(ssl->arrays->psk_key, 0, MAX_PSK_KEY_LEN);
@@ -6670,6 +6686,16 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
66706686
#endif
66716687
if (usingPSK)
66726688
*usingPSK = 0;
6689+
6690+
/* No PSK extension at all: if a mandatory external PSK is configured,
6691+
* refuse the connection rather than continue without one. havePSK is
6692+
* only set by an external-PSK callback, so a peer relying solely on
6693+
* session-ticket resumption is unaffected. */
6694+
if (ssl->options.havePSK && ssl->options.failNoPSK) {
6695+
WOLFSSL_ERROR_VERBOSE(PSK_MISSING_ERROR);
6696+
return PSK_MISSING_ERROR;
6697+
}
6698+
66736699
/* Hash data up to binders for deriving binders in PSK extension. */
66746700
ret = HashInput(ssl, input, (int)helloSz);
66756701
return ret;
@@ -6731,6 +6757,16 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
67316757
#endif
67326758

67336759
if (!*usingPSK) {
6760+
/* No suitable PSK was negotiated. When a mandatory external PSK is
6761+
* configured, fail with a dedicated error instead of falling back to a
6762+
* certificate handshake. This must run before the no-certificate
6763+
* BAD_BINDER check below so a PSK-only server (no cert) still reports
6764+
* PSK_MISSING_ERROR. havePSK is only set by an external-PSK callback, so
6765+
* a peer relying solely on session-ticket resumption is unaffected. */
6766+
if (ssl->options.havePSK && ssl->options.failNoPSK) {
6767+
WOLFSSL_ERROR_VERBOSE(PSK_MISSING_ERROR);
6768+
return PSK_MISSING_ERROR;
6769+
}
67346770
#ifndef NO_CERTS
67356771
if (ssl->buffers.certificate == NULL
67366772
#ifdef WOLFSSL_CERT_SETUP_CB
@@ -6892,7 +6928,11 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
68926928
ssl->namedGroup = ssl->session->namedGroup;
68936929
*usingPSK = 2; /* generate new ephemeral key */
68946930
}
6895-
else if (ssl->options.onlyPskDheKe) {
6931+
else if (ssl->options.onlyPskDheKe ||
6932+
(ssl->options.failNoPSK && !ssl->options.resuming)) {
6933+
/* A mandatory external PSK (failNoPSK) must be combined with
6934+
* (EC)DHE for forward secrecy, so reject a pure psk_ke
6935+
* negotiation. Session-ticket resumption is exempt. */
68966936
return PSK_KEY_ERROR;
68976937
}
68986938
else
@@ -6911,6 +6951,8 @@ static int CheckPreSharedKeys(WOLFSSL* ssl, const byte* input, word32 helloSz,
69116951
}
69126952
else {
69136953
#ifdef WOLFSSL_CERT_WITH_EXTERN_PSK
6954+
/* If no PSK is found, we remove the extension to make sure it
6955+
* is not sent back to the client */
69146956
TLSX_Remove(&ssl->extensions, TLSX_CERT_WITH_EXTERN_PSK, ssl->heap);
69156957
ssl->options.certWithExternPsk = 0;
69166958
#endif
@@ -11852,6 +11894,21 @@ int DoTls13Finished(WOLFSSL* ssl, const byte* input, word32* inOutIdx,
1185211894
}
1185311895
#endif
1185411896

11897+
#if !defined(NO_CERTS) && !defined(NO_PSK) && \
11898+
defined(WOLFSSL_CERT_WITH_EXTERN_PSK)
11899+
/* Verify the server sent a certificate if requested */
11900+
if (ssl->options.side == WOLFSSL_CLIENT_END && ssl->options.pskNegotiated &&
11901+
ssl->options.failNoCert) {
11902+
if ((TLSX_Find(ssl->extensions, TLSX_CERT_WITH_EXTERN_PSK) != NULL) &&
11903+
(!ssl->options.havePeerCert || !ssl->options.havePeerVerify)) {
11904+
ret = NO_PEER_CERT;
11905+
WOLFSSL_MSG("TLS v1.3 server did not present peer cert");
11906+
DoCertFatalAlert(ssl, ret);
11907+
goto cleanup;
11908+
}
11909+
}
11910+
#endif
11911+
1185511912
/* check against totalSz */
1185611913
if (*inOutIdx + size > totalSz) {
1185711914
ret = BUFFER_E;
@@ -14969,6 +15026,44 @@ int wolfSSL_only_dhe_psk(WOLFSSL* ssl)
1496915026
}
1497015027
#endif /* HAVE_SUPPORTED_CURVES */
1497115028

15029+
/* Require that an external Pre-Shared Key is negotiated for the handshake to
15030+
* succeed. TLS 1.3 / DTLS 1.3 only - in (D)TLS 1.2 the use of a PSK is
15031+
* determined by the negotiated cipher suite, so a mandatory PSK is configured
15032+
* there by restricting the cipher suite list to PSK suites.
15033+
*
15034+
* ctx The SSL/TLS CTX object.
15035+
* returns BAD_FUNC_ARG when ctx is NULL or not at least TLS v1.3, 0 on success.
15036+
*/
15037+
int wolfSSL_CTX_require_psk(WOLFSSL_CTX* ctx)
15038+
{
15039+
if (ctx == NULL || !IsAtLeastTLSv1_3(ctx->method->version))
15040+
return BAD_FUNC_ARG;
15041+
15042+
#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)
15043+
ctx->failNoPSK = 1;
15044+
#endif
15045+
15046+
return 0;
15047+
}
15048+
15049+
/* Require that an external Pre-Shared Key is negotiated for the handshake to
15050+
* succeed. See wolfSSL_CTX_require_psk().
15051+
*
15052+
* ssl The SSL/TLS object.
15053+
* returns BAD_FUNC_ARG when ssl is NULL or not at least TLS v1.3, 0 on success.
15054+
*/
15055+
int wolfSSL_require_psk(WOLFSSL* ssl)
15056+
{
15057+
if (ssl == NULL || !IsAtLeastTLSv1_3(ssl->version))
15058+
return BAD_FUNC_ARG;
15059+
15060+
#if defined(HAVE_SESSION_TICKET) || !defined(NO_PSK)
15061+
ssl->options.failNoPSK = 1;
15062+
#endif
15063+
15064+
return 0;
15065+
}
15066+
1497215067
int Tls13UpdateKeys(WOLFSSL* ssl)
1497315068
{
1497415069
if (ssl == NULL || !IsAtLeastTLSv1_3(ssl->version))

tests/api.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28384,7 +28384,7 @@ static int error_test(void)
2838428384
#endif
2838528385
{ -9, WC_SPAN1_FIRST_E + 1 },
2838628386
{ -300, -300 },
28387-
{ -335, -336 },
28387+
{ -336, -336 },
2838828388
{ -346, -349 },
2838928389
{ -356, -356 },
2839028390
{ -358, -358 },

0 commit comments

Comments
 (0)