Fixes from review

This commit is contained in:
Eric Blankenhorn
2026-06-12 12:33:56 -05:00
parent c5a211ca28
commit 552ad42111
10 changed files with 223 additions and 25 deletions
+14
View File
@@ -122,6 +122,20 @@ PR stands for Pull Request, and PR <NUMBER> references a GitHub pull request num
* **BREAKING (FIPS 205 SLH-DSA)**: `wc_SlhDsaKey_SignHash`, `wc_SlhDsaKey_SignHashDeterministic`, `wc_SlhDsaKey_SignHashWithRandom`, and `wc_SlhDsaKey_VerifyHash` now take the **caller-pre-hashed message digest** via `hash`/`hashSz` parameters (renamed from `msg`/`msgSz`), aligned with ML-DSA's `wc_dilithium_sign_ctx_hash` / `wc_dilithium_verify_ctx_hash` semantics, and NIST ACVP `signatureInterface=external` / `preHash=preHash` test vectors. `hashSz` must equal `wc_HashGetDigestSize(hashType)` (32 bytes for SHAKE128, 64 bytes for SHAKE256 per FIPS 205 Section 10.2.2); otherwise `BAD_LENGTH_E` is returned. Migration: hash the message yourself before the call (callers using positional arguments are source-compatible; only the parameter names changed). Caveat: callers who today pass a raw message whose length happens to equal the digest size for the chosen `hashType` (e.g., signing a 32-byte handle/IV/seed with `WC_HASH_TYPE_SHA256`) will not trip `BAD_LENGTH_E`; the resulting signature is syntactically valid but is over the wrong bytes. The pre-existing `wc_SlhDsaKey_SignMsgDeterministic` and `wc_SlhDsaKey_SignMsgWithRandom` retain their M'-supplied-directly contract (FIPS 205 internal interface, Algorithm 19); their input validation is hardened with the same NULL/length/`MISSING_KEY` checks as the `*Hash*` family. `wc_SlhDsaKey_VerifyMsg` is unchanged. All three gain doxygen coverage. (PR 10450, PR 10465)
* **Behavioral change (Raw Public Key fail-closed)**: With `HAVE_RPK`, a peer
that presents a Raw Public Key (RFC 7250) is no longer silently accepted while
it is being authenticated. Previously an RPK handshake always completed and
the application validated the key out of band afterward (e.g. via
`wolfSSL_get_verify_result()`); it now fails closed (with `RPK_UNTRUSTED_E`,
reported as `WOLFSSL_X509_V_ERR_RPK_UNTRUSTED`) whenever the peer is being
authenticated, i.e. any verify mode other than `WOLFSSL_VERIFY_NONE`. This
applies to all `HAVE_RPK` builds, including those without `OPENSSL_EXTRA` (no
prior untrusted-RPK handling) and `NO_SHA256` builds (no in-library pinning).
To establish trust, pin the expected key with the new
`wolfSSL_set_expected_rpk()` / `wolfSSL_CTX_set_expected_rpk()` APIs
(requires SHA-256), install a verify callback that accepts the key, or set
`WOLFSSL_VERIFY_NONE` to accept without authentication.
* **Behavioral change (RSA-PSS trailerField enforcement)**: `DecodeRsaPssParams`
(and its public wrapper `wc_DecodeRsaPssParams`) now enforces RFC 8017 A.2.3,
which mandates `trailerField == trailerFieldBC(1)`. In the default build
+55 -4
View File
@@ -16104,7 +16104,8 @@ int wolfSSL_get_negotiated_server_cert_type(WOLFSSL* ssl, int* tp);
the handshake fails closed unless the presented key matches a pin (or a verify
callback accepts it). May be called more than once to pin several keys, up to
WOLFSSL_MAX_RPK_PINS. The key is stored as its SHA-256 digest, so this API
requires SHA-256. Pins are append-only for the lifetime of the CTX.
requires SHA-256. Pins are append-only - there is no per-entry remove, but
wolfSSL_CTX_clear_expected_rpk() empties the table so it can be repopulated.
\return WOLFSSL_SUCCESS on success
\return BAD_FUNC_ARG if ctx or spki is NULL, or spkiSz is 0
@@ -16118,13 +16119,14 @@ int wolfSSL_get_negotiated_server_cert_type(WOLFSSL* ssl, int* tp);
\code
int ret;
WOLFSSL_CTX* ctx;
const unsigned char* spki; /* DER SubjectPublicKeyInfo */
const unsigned char* spki;
unsigned int spkiSz;
...
ret = wolfSSL_CTX_set_expected_rpk(ctx, spki, spkiSz);
\endcode
\sa wolfSSL_set_expected_rpk
\sa wolfSSL_CTX_clear_expected_rpk
\sa wolfSSL_set_client_cert_type
\sa wolfSSL_set_server_cert_type
*/
@@ -16140,7 +16142,8 @@ int wolfSSL_CTX_set_expected_rpk(WOLFSSL_CTX* ctx, const unsigned char* spki,
the handshake fails closed unless the presented key matches a pin (or a verify
callback accepts it). May be called more than once to pin several keys, up to
WOLFSSL_MAX_RPK_PINS. The key is stored as its SHA-256 digest, so this API
requires SHA-256. Pins are append-only for the lifetime of the object.
requires SHA-256. Pins are append-only - there is no per-entry remove, but
wolfSSL_clear_expected_rpk() empties the table so it can be repopulated.
\return WOLFSSL_SUCCESS on success
\return BAD_FUNC_ARG if ssl or spki is NULL, or spkiSz is 0
@@ -16154,19 +16157,67 @@ int wolfSSL_CTX_set_expected_rpk(WOLFSSL_CTX* ctx, const unsigned char* spki,
\code
int ret;
WOLFSSL* ssl;
const unsigned char* spki; /* DER SubjectPublicKeyInfo */
const unsigned char* spki;
unsigned int spkiSz;
...
ret = wolfSSL_set_expected_rpk(ssl, spki, spkiSz);
\endcode
\sa wolfSSL_CTX_set_expected_rpk
\sa wolfSSL_clear_expected_rpk
\sa wolfSSL_set_client_cert_type
\sa wolfSSL_set_server_cert_type
*/
int wolfSSL_set_expected_rpk(WOLFSSL* ssl, const unsigned char* spki,
unsigned int spkiSz);
/*!
\ingroup Setup
\brief Remove all pinned expected peer Raw Public Keys (RFC 7250) from the
WOLFSSL_CTX object, emptying the table so it can be repopulated - for example
across a peer key rotation, since the pinning APIs are otherwise append-only.
Like other WOLFSSL_CTX setters this is not locked, so reconfigure the pins on a
shared CTX while no handshakes are in flight (a WOLFSSL created with
wolfSSL_new() copies the pin table by value at creation time).
\return WOLFSSL_SUCCESS on success
\return BAD_FUNC_ARG if ctx is NULL
\param ctx WOLFSSL_CTX object pointer
_Example_
\code
WOLFSSL_CTX* ctx;
...
wolfSSL_CTX_clear_expected_rpk(ctx);
\endcode
\sa wolfSSL_CTX_set_expected_rpk
\sa wolfSSL_clear_expected_rpk
*/
int wolfSSL_CTX_clear_expected_rpk(WOLFSSL_CTX* ctx);
/*!
\ingroup Setup
\brief Remove all pinned expected peer Raw Public Keys (RFC 7250) from the
WOLFSSL object, emptying the table so it can be repopulated - for example
across a peer key rotation, since the pinning APIs are otherwise append-only.
\return WOLFSSL_SUCCESS on success
\return BAD_FUNC_ARG if ssl is NULL
\param ssl WOLFSSL object pointer
_Example_
\code
WOLFSSL* ssl;
...
wolfSSL_clear_expected_rpk(ssl);
\endcode
\sa wolfSSL_set_expected_rpk
\sa wolfSSL_CTX_clear_expected_rpk
*/
int wolfSSL_clear_expected_rpk(WOLFSSL* ssl);
/*!
\brief Enable use of ConnectionID extensions for the SSL object. See RFC 9146
+16 -10
View File
@@ -15216,6 +15216,7 @@ void DoCertFatalAlert(WOLFSSL* ssl, int ret)
alertWhy = certificate_expired;
}
else if (ret == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E) ||
ret == WC_NO_ERR_TRACE(RPK_UNTRUSTED_E) ||
ret == WC_NO_ERR_TRACE(ASN_PATHLEN_INV_E) ||
ret == WC_NO_ERR_TRACE(ASN_PATHLEN_SIZE_E)) {
alertWhy = unknown_ca;
@@ -17815,16 +17816,18 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
if (!rpkTrusted && !ssl->options.verifyNone) {
WOLFSSL_MSG("Untrusted RPK and peer verification "
"is on; failing handshake");
/* ASN_NO_SIGNER_E ("no trusted signer/anchor") is
* the closest existing error for an unpinned RPK.
* It is set here in the parse-SUCCESS branch, so the
* X.509 recovery that keys on this code (alternate
* chains, CA hash-dir lookup, Apple native
* validation) - all of which live in the
* parse-FAILURE branch above - is not reachable for
* RPK. The leaf verify callback is still invoked at
* TLS_ASYNC_FINALIZE and may override. */
ret = ASN_NO_SIGNER_E;
/* Use a dedicated RPK error rather than an
* X.509 one (e.g. ASN_NO_SIGNER_E) so the failure is
* distinguishable: GetX509Error() maps it to
* WOLFSSL_X509_V_ERR_RPK_UNTRUSTED, so a verify
* callback that accepts X.509 issuer-lookup errors
* (ASN_NO_SIGNER_E /
* WOLFSSL_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
* cannot accidentally accept an unauthenticated RPK.
* The leaf verify callback is still invoked at
* TLS_ASYNC_FINALIZE and may deliberately override
* this RPK-specific code. */
ret = RPK_UNTRUSTED_E;
WOLFSSL_ERROR_VERBOSE(ret);
}
}
@@ -29023,6 +29026,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e)
case SEQUENCE_NUMBER_E:
return "Record sequence number would wrap";
case RPK_UNTRUSTED_E:
return "RFC 7250 Raw Public Key not trusted";
}
return "unknown error number";
+32
View File
@@ -505,6 +505,38 @@ int wolfSSL_set_expected_rpk(WOLFSSL* ssl, const unsigned char* spki,
ret = rpk_add_expected(&ssl->options.rpkConfig, spki, spkiSz);
return (ret == 0) ? WOLFSSL_SUCCESS : ret;
}
/* Remove all pinned expected peer Raw Public Keys from the SSL/TLS CTX object,
* so the table can be repopulated (e.g. across a peer key rotation).
*
* @param [in] ctx SSL/TLS CTX object.
* @return WOLFSSL_SUCCESS on success.
* @return BAD_FUNC_ARG when ctx is NULL.
*/
int wolfSSL_CTX_clear_expected_rpk(WOLFSSL_CTX* ctx)
{
if (ctx == NULL) {
return BAD_FUNC_ARG;
}
ctx->rpkConfig.expectedRpkCnt = 0;
return WOLFSSL_SUCCESS;
}
/* Remove all pinned expected peer Raw Public Keys from the SSL/TLS object, so
* the table can be repopulated (e.g. across a peer key rotation).
*
* @param [in] ssl SSL/TLS object.
* @return WOLFSSL_SUCCESS on success.
* @return BAD_FUNC_ARG when ssl is NULL.
*/
int wolfSSL_clear_expected_rpk(WOLFSSL* ssl)
{
if (ssl == NULL) {
return BAD_FUNC_ARG;
}
ssl->options.rpkConfig.expectedRpkCnt = 0;
return WOLFSSL_SUCCESS;
}
#endif /* !NO_SHA256 */
#endif /* HAVE_RPK */
+6
View File
@@ -282,6 +282,12 @@ int GetX509Error(int e)
case WC_NO_ERR_TRACE(ASN_NO_SIGNER_E):
/* get issuer error if no CA found locally */
return WOLFSSL_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY;
case WC_NO_ERR_TRACE(RPK_UNTRUSTED_E):
/* RFC 7250 Raw Public Key not trusted out of band. Distinct from
* the X.509 issuer-lookup error above so verify callbacks that
* accept ASN_NO_SIGNER_E / UNABLE_TO_GET_ISSUER_CERT_LOCALLY do not
* accidentally accept an unauthenticated RPK. */
return WOLFSSL_X509_V_ERR_RPK_UNTRUSTED;
case WC_NO_ERR_TRACE(ASN_SELF_SIGNED_E):
return WOLFSSL_X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT;
case WC_NO_ERR_TRACE(ASN_PATHLEN_INV_E):
+6 -1
View File
@@ -28912,7 +28912,12 @@ static int error_test(void)
}
}
#if defined(OPENSSL_EXTRA)
/* Guard matches the compilation condition of the OpenSSL-style reason
* strings (wolfSSL_ERR_reason_error_string_OpenSSL), so the RPK string is
* exercised in OPENSSL_EXTRA_X509_SMALL / webserver / memcached builds too,
* not only OPENSSL_EXTRA. */
#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) || \
defined(HAVE_WEBSERVER) || defined(HAVE_MEMCACHED)
/* WOLFSSL_X509_V_ERR_RPK_UNTRUSTED is >= WC_OSSL_V509_V_ERR_MAX, so it is
* intentionally outside the contiguous sweep above. Check its reason string
* explicitly so it cannot regress silently. error_test() is invoked as
+74
View File
@@ -3703,6 +3703,27 @@ static int test_rpk_accept_cb(int preverify, WOLFSSL_X509_STORE_CTX* store)
(void)store;
return 1;
}
/* Mimics a real-world X.509 verify callback that accepts ONLY the "issuer not
* found locally" errors (the pattern used elsewhere to allow an unchained or
* self-signed leaf). It must NOT rescue an untrusted RPK: because the RPK
* failure now reports WOLFSSL_X509_V_ERR_RPK_UNTRUSTED (not the X.509
* issuer-lookup codes), these checks no longer match and the RPK stays rejected.
* Returns 1 (accept) only for the X.509 issuer codes, otherwise rejects. */
static int test_rpk_x509_issuer_accept_cb(int preverify,
WOLFSSL_X509_STORE_CTX* store)
{
(void)preverify;
if ((store->error == WC_NO_ERR_TRACE(ASN_NO_SIGNER_E))
#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL)
|| (store->error ==
WOLFSSL_X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
#endif
) {
return 1; /* would wrongly accept an RPK that reused these codes */
}
return 0; /* reject everything else, including WOLFSSL_X509_V_ERR_RPK_* */
}
#endif /* HAVE_RPK && WOLFSSL_TLS13 && client && server && !NO_SHA256 */
#if defined(HAVE_RPK) && \
@@ -3926,6 +3947,31 @@ int test_tls13_rpk_trust(void)
wolfSSL_CTX_free(ctx_c);
wolfSSL_CTX_free(ctx_s);
/* --- a callback that accepts only X.509 issuer-lookup errors must NOT
* rescue an untrusted RPK: the RPK failure reports its own error code, so
* the callback's X.509 checks do not match and the handshake fails closed.
* (Would complete if the RPK reused ASN_NO_SIGNER_E / the issuer-lookup
* verify-result code.) --- */
ctx_c = ctx_s = NULL;
ssl_c = ssl_s = NULL;
XMEMSET(&test_ctx, 0, sizeof(test_ctx));
ExpectIntEQ(test_rpk_nopin_setup(&test_ctx, &ctx_c, &ctx_s,
&ssl_c, &ssl_s, wolfTLSv1_3_client_method,
wolfTLSv1_3_server_method), 0);
wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER,
test_rpk_x509_issuer_accept_cb);
wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER,
test_rpk_x509_issuer_accept_cb);
ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0);
#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL)
ExpectIntEQ(wolfSSL_get_verify_result(ssl_c),
WOLFSSL_X509_V_ERR_RPK_UNTRUSTED);
#endif
wolfSSL_free(ssl_c);
wolfSSL_free(ssl_s);
wolfSSL_CTX_free(ctx_c);
wolfSSL_CTX_free(ctx_s);
/* --- SSL-level pin via wolfSSL_set_expected_rpk() -> V_OK --- */
ctx_c = ctx_s = NULL;
ssl_c = ssl_s = NULL;
@@ -3995,9 +4041,37 @@ int test_tls13_rpk_trust(void)
}
ExpectIntEQ(wolfSSL_set_expected_rpk(ssl_c, svrSpki,
(unsigned int)svrSpkiSz), WC_NO_ERR_TRACE(BUFFER_E));
/* clear empties the (full) table so an add succeeds again */
ExpectIntEQ(wolfSSL_clear_expected_rpk(NULL), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
ExpectIntEQ(wolfSSL_clear_expected_rpk(ssl_c), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_set_expected_rpk(ssl_c, svrSpki,
(unsigned int)svrSpkiSz), WOLFSSL_SUCCESS);
wolfSSL_free(ssl_c);
wolfSSL_CTX_free(ctx_c);
/* same argument/capacity errors for the CTX-level wrapper */
ctx_c = NULL;
ExpectNotNull(ctx_c = wolfSSL_CTX_new(wolfTLSv1_3_client_method()));
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(NULL, svrSpki,
(unsigned int)svrSpkiSz), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(ctx_c, NULL,
(unsigned int)svrSpkiSz), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(ctx_c, svrSpki, 0),
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
for (i = 0; i < WOLFSSL_MAX_RPK_PINS; i++) {
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(ctx_c, svrSpki,
(unsigned int)svrSpkiSz), WOLFSSL_SUCCESS);
}
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(ctx_c, svrSpki,
(unsigned int)svrSpkiSz), WC_NO_ERR_TRACE(BUFFER_E));
/* clear empties the (full) table so an add succeeds again */
ExpectIntEQ(wolfSSL_CTX_clear_expected_rpk(NULL),
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
ExpectIntEQ(wolfSSL_CTX_clear_expected_rpk(ctx_c), WOLFSSL_SUCCESS);
ExpectIntEQ(wolfSSL_CTX_set_expected_rpk(ctx_c, svrSpki,
(unsigned int)svrSpkiSz), WOLFSSL_SUCCESS);
wolfSSL_CTX_free(ctx_c);
XFREE(svrSpki, NULL, DYNAMIC_TYPE_TMP_BUFFER);
XFREE(cliSpki, NULL, DYNAMIC_TYPE_TMP_BUFFER);
#endif /* HAVE_RPK && WOLFSSL_TLS13 && client && server && !NO_SHA256 */
+4 -1
View File
@@ -247,7 +247,10 @@ enum wolfSSL_ErrorCodes {
SEQUENCE_NUMBER_E = -520, /* Record sequence number would wrap */
WOLFSSL_LAST_E = -520
RPK_UNTRUSTED_E = -521, /* RFC 7250 Raw Public Key not trusted
* out of band */
WOLFSSL_LAST_E = -521
/* codes -1000 to -1999 are reserved for wolfCrypt. */
};
+2 -3
View File
@@ -210,9 +210,8 @@
#define X509_V_ERR_CA_CERT_MISSING_KEY_USAGE 92
#define X509_V_ERR_EXTENSIONS_REQUIRE_VERSION_3 93
#define X509_V_ERR_EC_KEY_EXPLICIT_PARAMS 94
/* wolfSSL-specific value: this header's numbering already diverges from
* OpenSSL's for the higher X509_V_ERR_* codes, so RPK-untrusted takes the next
* free wolfSSL slot (95) rather than any particular OpenSSL number. */
/* 95 matches OpenSSL's X509_V_ERR_RPK_UNTRUSTED (OpenSSL 3.2+) for source
* compatibility. */
#define X509_V_ERR_RPK_UNTRUSTED 95
#define X509_R_CERT_ALREADY_IN_HASH_TABLE 101
#define X509_R_KEY_VALUES_MISMATCH WC_KEY_MISMATCH_E
+14 -6
View File
@@ -2782,9 +2782,9 @@ enum {
WOLFSSL_X509_V_ERR_IP_ADDRESS_MISMATCH = 64,
WOLFSSL_X509_V_ERR_INVALID_CA = 79,
WC_OSSL_V509_V_ERR_MAX = 80,
/* wolfSSL-specific verify-result codes are assigned at or above
* WC_OSSL_V509_V_ERR_MAX, intentionally outside the contiguous
* OpenSSL-compatible range below it. */
/* 95 matches OpenSSL's X509_V_ERR_RPK_UNTRUSTED (OpenSSL 3.2+). It is
* deliberately at or above WC_OSSL_V509_V_ERR_MAX, i.e. outside the
* contiguous block of verify-result codes below it. */
WOLFSSL_X509_V_ERR_RPK_UNTRUSTED = 95,
#ifdef HAVE_OCSP
@@ -6258,15 +6258,23 @@ WOLFSSL_API int wolfSSL_get_negotiated_server_cert_type(WOLFSSL* ssl, int* tp);
* stored as its SHA-256 digest, so these APIs require SHA-256; without it,
* express RPK trust through a verify callback. Returns WOLFSSL_SUCCESS, or a
* negative error code (BAD_FUNC_ARG for a NULL argument or zero length, BUFFER_E
* when the pin table is full). Pins are append-only for the lifetime of the
* object/CTX; there is no clear/replace call, so a long-lived CTX that re-pins
* across key rotations can exhaust the WOLFSSL_MAX_RPK_PINS-entry table. */
* when the pin table is full). Pins are append-only: there is no per-entry
* remove, but wolfSSL_clear_expected_rpk()/wolfSSL_CTX_clear_expected_rpk()
* empties the WOLFSSL_MAX_RPK_PINS-entry table so it can be repopulated (e.g.
* across a peer key rotation). */
WOLFSSL_API int wolfSSL_CTX_set_expected_rpk(WOLFSSL_CTX* ctx,
const unsigned char* spki,
unsigned int spkiSz);
WOLFSSL_API int wolfSSL_set_expected_rpk(WOLFSSL* ssl,
const unsigned char* spki,
unsigned int spkiSz);
/* Remove all pinned expected peer Raw Public Keys, emptying the table so it can
* be repopulated. Returns WOLFSSL_SUCCESS, or BAD_FUNC_ARG when the handle is
* NULL. Like the set calls and other CTX setters these are unlocked, so
* reconfigure pins on a shared CTX while no handshakes are in flight (each
* WOLFSSL copies the pin table by value at wolfSSL_new()). */
WOLFSSL_API int wolfSSL_CTX_clear_expected_rpk(WOLFSSL_CTX* ctx);
WOLFSSL_API int wolfSSL_clear_expected_rpk(WOLFSSL* ssl);
#endif /* !NO_SHA256 */
#endif /* HAVE_RPK */