Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
123 changes: 123 additions & 0 deletions doc/dox_comments/header_files/ssl.h
Original file line number Diff line number Diff line change
Expand Up @@ -16095,6 +16095,129 @@ int wolfSSL_get_negotiated_client_cert_type(WOLFSSL* ssl, int* tp);
*/
int wolfSSL_get_negotiated_server_cert_type(WOLFSSL* ssl, int* tp);

/*!
\ingroup Setup
\brief Pin a DER-encoded SubjectPublicKeyInfo that the peer is expected to
present as a Raw Public Key (RFC 7250), establishing out-of-band trust on the
WOLFSSL_CTX object. An unauthenticated RPK peer is otherwise rejected: when the
peer is being authenticated (any verify mode other than WOLFSSL_VERIFY_NONE)
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 - 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
\return BUFFER_E if the pin table is already full (WOLFSSL_MAX_RPK_PINS)
\return other negative error code on a hashing failure

\param ctx WOLFSSL_CTX object pointer
\param spki DER-encoded SubjectPublicKeyInfo the peer is expected to present
\param spkiSz length of spki in bytes
_Example_
\code
int ret;
WOLFSSL_CTX* ctx;
Comment thread
embhorn marked this conversation as resolved.
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
*/
int wolfSSL_CTX_set_expected_rpk(WOLFSSL_CTX* ctx, const unsigned char* spki,
unsigned int spkiSz);

/*!
\ingroup Setup
\brief Pin a DER-encoded SubjectPublicKeyInfo that the peer is expected to
present as a Raw Public Key (RFC 7250), establishing out-of-band trust on the
WOLFSSL object. An unauthenticated RPK peer is otherwise rejected: when the
peer is being authenticated (any verify mode other than WOLFSSL_VERIFY_NONE)
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 - 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
\return BUFFER_E if the pin table is already full (WOLFSSL_MAX_RPK_PINS)
\return other negative error code on a hashing failure

\param ssl WOLFSSL object pointer
\param spki DER-encoded SubjectPublicKeyInfo the peer is expected to present
\param spkiSz length of spki in bytes
_Example_
\code
int ret;
WOLFSSL* ssl;
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
Expand Down
116 changes: 106 additions & 10 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -16985,6 +16986,43 @@ static int ProcessPeerCertDecodeKey(WOLFSSL* ssl, ProcPeerCertArgs* args,
return 0;
}

#if defined(HAVE_RPK)
/* Determine whether a received Raw Public Key (RFC 7250) has been pinned as
* trusted out of band. The on-wire RPK is exactly the DER SubjectPublicKeyInfo,
* so its SHA-256 digest is compared against the application-supplied pins.
*
* @param [in] ssl SSL/TLS object.
* @param [in] spki DER SubjectPublicKeyInfo received from the peer.
* @param [in] spkiSz Length of spki in bytes.
* @return 1 when the key matches a pin, 0 otherwise.
*/
static int RpkIsTrusted(WOLFSSL* ssl, const byte* spki, word32 spkiSz)
{
#ifndef NO_SHA256
RpkConfig* cfg = &ssl->options.rpkConfig;

if ((cfg->expectedRpkCnt > 0) && (spki != NULL) && (spkiSz > 0)) {
byte digest[WC_SHA256_DIGEST_SIZE];
int i;

if (wc_Sha256Hash(spki, spkiSz, digest) == 0) {
for (i = 0; i < (int)cfg->expectedRpkCnt; i++) {
if (XMEMCMP(digest, cfg->expectedRpk[i],
WC_SHA256_DIGEST_SIZE) == 0) {
return 1;
}
}
}
}
#else
(void)ssl;
(void)spki;
(void)spkiSz;
#endif /* !NO_SHA256 */
return 0;
}
#endif /* HAVE_RPK */

int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
word32 totalSz)
{
Expand Down Expand Up @@ -17736,6 +17774,65 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
if (ssl->peerVerifyRet == 0) /* Return first cert error here */
ssl->peerVerifyRet = WOLFSSL_X509_V_OK;
#endif

#if defined(HAVE_RPK)
/* A Raw Public Key cert (RFC 7250) has no issuer and no
* signature, so ParseCertRelative performed no peer
* authentication. The key is only authenticated if it was
* pinned out of band (expected RPK). Applies to both peers
* - client checking the server's RPK and server checking
* the client's RPK. */
if (args->dCert->isRPK) {
int rpkTrusted = RpkIsTrusted(ssl,
args->certs[args->certIdx].buffer,
args->certs[args->certIdx].length);

#if defined(OPENSSL_EXTRA) || \
defined(OPENSSL_EXTRA_X509_SMALL)
/* Report the status truthfully regardless of verify
* mode, mirroring OpenSSL: get_verify_result() reflects
* the actual key status even under WOLFSSL_VERIFY_NONE
* rather than leaving it at WOLFSSL_X509_V_OK. */
if (!rpkTrusted &&
ssl->peerVerifyRet == WOLFSSL_X509_V_OK) {
ssl->peerVerifyRet = (unsigned long)
WOLFSSL_X509_V_ERR_RPK_UNTRUSTED;
}
#endif /* OPENSSL_EXTRA || OPENSSL_EXTRA_X509_SMALL */

/* Fail closed when the peer is being authenticated: an
Comment thread
embhorn marked this conversation as resolved.
* untrusted RPK must abort the handshake rather than
* silently complete it.
* The gate is !verifyNone, matching the X.509
* authentication gate above (the VERIFY/NO_VERIFY
* choice passed to ProcessPeerCertParse) - so a default
* authenticating client (verifyPeer unset, verifyNone
* unset) validating an RPK server is protected too, not
* just the explicit WOLFSSL_VERIFY_PEER case. The verify
* callback can still override, exactly as it can for an
* X.509 chain error. WOLFSSL_VERIFY_NONE keeps the
* "accept and let the application decide" behaviour per
* RFC 7250. */
if (!rpkTrusted && !ssl->options.verifyNone) {
WOLFSSL_MSG("Untrusted RPK and peer verification "
"is on; failing handshake");
/* 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);
}
}
#endif /* HAVE_RPK */

#if defined(SESSION_CERTS) && defined(WOLFSSL_ALT_CERT_CHAINS)
/* if using alternate chain, store the cert used */
if (ssl->options.usingAltCertChain) {
Expand All @@ -17754,17 +17851,10 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
if (ssl->options.side == WOLFSSL_SERVER_END) {
#if defined(HAVE_RPK)
if (args->dCert->isRPK) {
/* to verify Raw Public Key cert, DANE(RFC6698)
* should be introduced. Without DANE, no
* authentication is performed.
*/
#if defined(HAVE_DANE)
if (ssl->useDANE) {
/* DANE authentication should be added */
}
#endif /* HAVE_DANE */
/* RPK certs carry no X.509 version; the RPK trust
* check above already handled this cert. */
}
else /* skip followingx509 version check */
else /* skip following x509 version check */
#endif /* HAVE_RPK */
if (args->dCert->version != WOLFSSL_X509_V3) {
WOLFSSL_MSG("Peers certificate was not version 3!");
Expand Down Expand Up @@ -28330,6 +28420,9 @@ static const char* wolfSSL_ERR_reason_error_string_OpenSSL(unsigned long e)
case WOLFSSL_X509_V_ERR_IP_ADDRESS_MISMATCH:
return "IP address mismatch";

case WOLFSSL_X509_V_ERR_RPK_UNTRUSTED:
return "raw public key not trusted";

default:
return NULL;
}
Expand Down Expand Up @@ -28933,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";
Expand Down
Loading
Loading