Skip to content

Policy Rejection Of A Valid PSK Reports handshake_failure Instead Of access_denied #11077

Description

@LiD0209

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions