Policy Rejection Of A Valid PSK Reports handshake_failure Instead Of access_denied
Problem Description
Focused runtime verification confirmed a real alert-mapping issue in wolfSSL's TLS 1.3 external-PSK path: when the server recognizes a valid offered PSK identity and then rejects it by local policy, the connection fails with handshake_failure(40) rather than access_denied(49).
This revised report narrows the original claim. RFC 8446 Section 6.2 defines access_denied as the alert meaning for a valid certificate or PSK rejected after access control, but the surrounding text frames alert selection here as sending an appropriate fatal alert rather than an explicit per-site "MUST send access_denied" rule. The confirmed issue is therefore a real semantic mismatch in alert selection, not the strongest form of textual MUST-violation.
Standard Requirement
- Official standard: RFC 8446
- Section: RFC 8446 Section 6.2 Error Alerts
Relevant RFC 8446 context:
Whenever an implementation encounters a fatal error condition, it
SHOULD send an appropriate fatal alert and MUST close the connection
without sending or receiving any additional data. In the rest of
this specification, when the phrases "terminate the connection" and
"abort the handshake" are used without a specific alert it means that
the implementation SHOULD send the alert indicated by the
descriptions below. The phrases "terminate the connection with an X
alert" and "abort the handshake with an X alert" mean that the
implementation MUST send alert X if it sends any alert.
access_denied: A valid certificate or PSK was received, but when
access control was applied, the sender decided not to proceed with
negotiation.
Interpretation:
- Section 6.2 defines
access_denied as the alert description that matches the "valid certificate or PSK, then local access-control rejection" condition.
- The same section also says that, absent a more specific "abort with X alert" instruction elsewhere, the implementation
SHOULD send the alert indicated by the alert descriptions.
- Therefore the standards basis here is best read as:
access_denied is the semantically appropriate alert for this condition, but this specific passage is not itself phrased as an explicit "MUST send access_denied" command.
Relevant Source Code
The implementation defines the access_denied codepoint, but the TLS 1.3 external-PSK policy-rejection path does not use it.
src/tls13.c:6385-6390
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);
}
The server PSK callback returning 0 is treated uniformly as "no usable PSK found". The code does not distinguish "unknown identity" from "known valid identity rejected by local policy".
src/tls13.c:6853-6861
/* No suitable PSK was negotiated. When a mandatory external PSK is
* configured, fail with a dedicated error instead of falling back to a
* certificate handshake. This must run before the no-certificate
* BAD_BINDER check below so a PSK-only server (no cert) still reports
* PSK_MISSING_ERROR. havePSK is only set by an external-PSK callback, so
* a peer relying solely on session-ticket resumption is unaffected. */
if (ssl->options.havePSK && ssl->options.failNoPSK) {
WOLFSSL_ERROR_VERBOSE(PSK_MISSING_ERROR);
return PSK_MISSING_ERROR;
}
When PSK use is mandatory, the "callback returned 0" outcome is normalized to PSK_MISSING_ERROR.
src/internal.c:37153-37173
int TranslateErrorToAlert(int err)
{
switch (err) {
case WC_NO_ERR_TRACE(BUFFER_ERROR):
return decode_error;
case WC_NO_ERR_TRACE(EXT_NOT_ALLOWED):
case WC_NO_ERR_TRACE(PEER_KEY_ERROR):
case WC_NO_ERR_TRACE(ECC_PEERKEY_ERROR):
case WC_NO_ERR_TRACE(BAD_KEY_SHARE_DATA):
case WC_NO_ERR_TRACE(PSK_KEY_ERROR):
case WC_NO_ERR_TRACE(INVALID_PARAMETER):
case WC_NO_ERR_TRACE(HRR_COOKIE_ERROR):
case WC_NO_ERR_TRACE(BAD_BINDER):
case WC_NO_ERR_TRACE(DUPLICATE_TLS_EXT_E):
return illegal_parameter;
case WC_NO_ERR_TRACE(INCOMPLETE_DATA):
return missing_extension;
case WC_NO_ERR_TRACE(MATCH_SUITE_ERROR):
case WC_NO_ERR_TRACE(MISSING_HANDSHAKE_DATA):
case WC_NO_ERR_TRACE(PSK_MISSING_ERROR):
return handshake_failure;
PSK_MISSING_ERROR is translated to handshake_failure, which is the alert observed at runtime.
Runtime Evidence
The original family runtime wrapper did not have a native reproducer for this requirement family. A focused local probe was built and run to confirm the actual alert on the wire.
Focused Probe
The probe offered the same valid external PSK identity twice. The control server accepted it and completed the handshake. In the second run, the server callback recognized the identity but rejected it by policy while the probe captured the alert sent on the wire.
Observed result:
{
"positive_control": {
"policy_reject": false,
"server_saw_valid_identity": true,
"client_connect_ret": 1,
"client_error": 0,
"client_last_tx_alert": {"level": -1, "code": -1, "name": "none"},
"client_last_rx_alert": {"level": -1, "code": -1, "name": "none"},
"server_accept_ret": 1,
"server_error": 0,
"server_last_tx_alert": {"level": -1, "code": -1, "name": "none"},
"server_last_rx_alert": {"level": -1, "code": -1, "name": "none"}
},
"policy_reject": {
"policy_reject": true,
"server_saw_valid_identity": true,
"client_connect_ret": -1,
"client_error": -313,
"client_last_tx_alert": {"level": -1, "code": -1, "name": "none"},
"client_last_rx_alert": {"level": 2, "code": 40, "name": "handshake_failure"},
"server_accept_ret": -1,
"server_error": -335,
"server_last_tx_alert": {"level": 2, "code": 40, "name": "handshake_failure"},
"server_last_rx_alert": {"level": -1, "code": -1, "name": "none"}
}
}
What this proves:
- The positive control shows the same offered PSK identity is valid and accepted when policy permits it.
- In the rejection case,
server_saw_valid_identity: true proves the server recognized that same valid identity.
- Despite that, the transmitted fatal alert is
handshake_failure(40), not access_denied(49).
Inconsistency Reason
RFC 8446 defines access_denied as the alert meaning for "valid certificate or PSK received, but rejected by access control". wolfSSL's external-PSK rejection path does not preserve that distinction. Once the server callback returns 0, the implementation collapses the outcome into "no suitable PSK negotiated", raises PSK_MISSING_ERROR, and then translates that error into handshake_failure.
That means wolfSSL conflates two different conditions:
- no acceptable/usable PSK was found, and
- a valid PSK identity was recognized but rejected by local policy.
The runtime probe confirmed this is not merely a static suspicion: the implementation really emits handshake_failure on the wire for the second case.
Decision Reason
- The earlier
suspected_issue verdict was too weak because it lacked a path-specific reproducer.
- The focused probe now confirms the actual emitted alert and removes the runtime uncertainty.
- The report should still avoid overstating the standards basis: this is a confirmed alert-selection mismatch against the RFC 8446
access_denied description, but the exact text at Section 6.2 is better read as a semantic SHOULD-level fit than as an explicit per-site MUST-send rule.
Remaining Uncertainty
- This confirmation covers the TLS 1.3 external-PSK policy-rejection path.
- A separate runtime probe for a certificate-validation-success-then-policy-reject path was not added here, so the confirmed evidence is strongest for PSK rather than certificate-based access-control rejection.
Policy Rejection Of A Valid PSK Reports
handshake_failureInstead Ofaccess_deniedProblem Description
Focused runtime verification confirmed a real alert-mapping issue in wolfSSL's TLS 1.3 external-PSK path: when the server recognizes a valid offered PSK identity and then rejects it by local policy, the connection fails with
handshake_failure(40)rather thanaccess_denied(49).This revised report narrows the original claim. RFC 8446 Section 6.2 defines
access_deniedas the alert meaning for a valid certificate or PSK rejected after access control, but the surrounding text frames alert selection here as sending an appropriate fatal alert rather than an explicit per-site "MUST send access_denied" rule. The confirmed issue is therefore a real semantic mismatch in alert selection, not the strongest form of textual MUST-violation.Standard Requirement
Relevant RFC 8446 context:
Interpretation:
access_deniedas the alert description that matches the "valid certificate or PSK, then local access-control rejection" condition.SHOULDsend the alert indicated by the alert descriptions.access_deniedis the semantically appropriate alert for this condition, but this specific passage is not itself phrased as an explicit "MUST send access_denied" command.Relevant Source Code
The implementation defines the
access_deniedcodepoint, but the TLS 1.3 external-PSK policy-rejection path does not use it.src/tls13.c:6385-6390The server PSK callback returning
0is treated uniformly as "no usable PSK found". The code does not distinguish "unknown identity" from "known valid identity rejected by local policy".src/tls13.c:6853-6861When PSK use is mandatory, the "callback returned 0" outcome is normalized to
PSK_MISSING_ERROR.src/internal.c:37153-37173PSK_MISSING_ERRORis translated tohandshake_failure, which is the alert observed at runtime.Runtime Evidence
The original family runtime wrapper did not have a native reproducer for this requirement family. A focused local probe was built and run to confirm the actual alert on the wire.
Focused Probe
The probe offered the same valid external PSK identity twice. The control server accepted it and completed the handshake. In the second run, the server callback recognized the identity but rejected it by policy while the probe captured the alert sent on the wire.
Observed result:
{ "positive_control": { "policy_reject": false, "server_saw_valid_identity": true, "client_connect_ret": 1, "client_error": 0, "client_last_tx_alert": {"level": -1, "code": -1, "name": "none"}, "client_last_rx_alert": {"level": -1, "code": -1, "name": "none"}, "server_accept_ret": 1, "server_error": 0, "server_last_tx_alert": {"level": -1, "code": -1, "name": "none"}, "server_last_rx_alert": {"level": -1, "code": -1, "name": "none"} }, "policy_reject": { "policy_reject": true, "server_saw_valid_identity": true, "client_connect_ret": -1, "client_error": -313, "client_last_tx_alert": {"level": -1, "code": -1, "name": "none"}, "client_last_rx_alert": {"level": 2, "code": 40, "name": "handshake_failure"}, "server_accept_ret": -1, "server_error": -335, "server_last_tx_alert": {"level": 2, "code": 40, "name": "handshake_failure"}, "server_last_rx_alert": {"level": -1, "code": -1, "name": "none"} } }What this proves:
server_saw_valid_identity: trueproves the server recognized that same valid identity.handshake_failure(40), notaccess_denied(49).Inconsistency Reason
RFC 8446 defines
access_deniedas the alert meaning for "valid certificate or PSK received, but rejected by access control". wolfSSL's external-PSK rejection path does not preserve that distinction. Once the server callback returns0, the implementation collapses the outcome into "no suitable PSK negotiated", raisesPSK_MISSING_ERROR, and then translates that error intohandshake_failure.That means wolfSSL conflates two different conditions:
The runtime probe confirmed this is not merely a static suspicion: the implementation really emits
handshake_failureon the wire for the second case.Decision Reason
suspected_issueverdict was too weak because it lacked a path-specific reproducer.access_denieddescription, but the exact text at Section 6.2 is better read as a semanticSHOULD-level fit than as an explicit per-site MUST-send rule.Remaining Uncertainty