diff --git a/.github/workflows/openssl-ech.yml b/.github/workflows/openssl-ech.yml index e295062c71..76bdbad975 100644 --- a/.github/workflows/openssl-ech.yml +++ b/.github/workflows/openssl-ech.yml @@ -24,7 +24,9 @@ jobs: with: path: wolfssl configure: >- - --enable-ech --enable-sha512 --enable-aes CFLAGS='-DUSE_FLAT_TEST_H' + --enable-ech --enable-sha512 --enable-aes + CFLAGS='-DUSE_FLAT_TEST_H -DWOLFSSL_TEST_ECH' + check: true install: true - name: tar build-dir diff --git a/src/internal.c b/src/internal.c index 2ba6cabc15..7ff97de136 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8773,6 +8773,10 @@ void wolfSSL_ResourceFree(WOLFSSL* ssl) FreeEchConfigs(ssl->echConfigs, ssl->heap); ssl->echConfigs = NULL; } + if (ssl->echRetryConfigs != NULL) { + FreeEchConfigs(ssl->echRetryConfigs, ssl->heap); + ssl->echRetryConfigs = NULL; + } #endif /* HAVE_ECH */ #endif /* WOLFSSL_TLS13 */ #ifdef WOLFSSL_HAVE_TLS_UNIQUE @@ -15713,6 +15717,8 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, byte* subjectHash = NULL; int alreadySigner = 0; + char* domainName = NULL; + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) int addToPendingCAs = 0; #endif @@ -16901,17 +16907,34 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif - if (!ssl->options.verifyNone && ssl->buffers.domainName.buffer) { + domainName = (char*)ssl->buffers.domainName.buffer; + #if !defined(NO_WOLFSSL_CLIENT) && defined(HAVE_ECH) + /* RFC 9849 s6.1.7: ECH offered but rejected by the server... + * verify cert is valid for ECHConfig.public_name */ + if (ssl->options.side == WOLFSSL_CLIENT_END && + ssl->echConfigs != NULL && + !ssl->options.echAccepted) { + TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); + if (echX != NULL && echX->data != NULL) { + WOLFSSL_ECH* ech = (WOLFSSL_ECH*)echX->data; + if (ech->echConfig != NULL && + ech->echConfig->publicName != NULL) { + domainName = ech->echConfig->publicName; + } + } + } + #endif + + if (!ssl->options.verifyNone && domainName) { #ifndef WOLFSSL_ALLOW_NO_CN_IN_SAN /* Per RFC 5280 section 4.2.1.6, "Whenever such identities * are to be bound into a certificate, the subject * alternative name extension MUST be used." */ if (args->dCert->altNames) { - if (CheckForAltNames(args->dCert, - (char*)ssl->buffers.domainName.buffer, - (ssl->buffers.domainName.buffer == NULL ? 0 : - (word32)XSTRLEN( - (const char *)ssl->buffers.domainName.buffer)), + if (CheckForAltNames( + args->dCert, + domainName, + (word32)XSTRLEN((const char *)domainName), NULL, 0, 0) != 1) { WOLFSSL_MSG("DomainName match on alt names failed"); /* try to get peer key still */ @@ -16924,11 +16947,9 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, if (MatchDomainName( args->dCert->subjectCN, args->dCert->subjectCNLen, - (char*)ssl->buffers.domainName.buffer, - (ssl->buffers.domainName.buffer == NULL ? 0 : - (word32)XSTRLEN( - (const char *)ssl->buffers.domainName.buffer) - ), 0) == 0) + domainName, + (word32)XSTRLEN((const char *)domainName), + 0) == 0) #endif { WOLFSSL_MSG("DomainName match failed"); @@ -16939,18 +16960,19 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #else /* WOLFSSL_ALL_NO_CN_IN_SAN */ /* Old behavior. */ #ifndef WOLFSSL_HOSTNAME_VERIFY_ALT_NAME_ONLY - if (MatchDomainName(args->dCert->subjectCN, + if (MatchDomainName( + args->dCert->subjectCN, args->dCert->subjectCNLen, - (char*)ssl->buffers.domainName.buffer, - (ssl->buffers.domainName.buffer == NULL ? 0 : - (word32)XSTRLEN(ssl->buffers.domainName.buffer)), 0) == 0) + domainName, + (word32)XSTRLEN((const char *)domainName), + 0) == 0) #endif { - if (CheckForAltNames(args->dCert, - (char*)ssl->buffers.domainName.buffer, - (ssl->buffers.domainName.buffer == NULL ? 0 : - (word32)XSTRLEN(ssl->buffers.domainName.buffer)), - NULL, 0, 0) != 1) { + if (CheckForAltNames( + args->dCert, + domainName, + (word32)XSTRLEN((const char *)domainName), + NULL, 0, 0) != 1) { WOLFSSL_MSG("DomainName match failed"); /* try to get peer key still */ ret = DOMAIN_NAME_MISMATCH; @@ -22160,6 +22182,13 @@ const char* AlertTypeToString(int type) return no_application_protocol_str; } + case ech_required: + { + static const char ech_required_str[] = + "ech_required"; + return ech_required_str; + } + default: WOLFSSL_MSG("Unknown Alert"); return NULL; @@ -27766,6 +27795,9 @@ const char* wolfSSL_ERR_reason_error_string(unsigned long e) case SESSION_TICKET_NONCE_OVERFLOW: return "Session ticket nonce overflow"; + + case ECH_REQUIRED_E: + return "ECH offered but rejected by server"; } return "unknown error number"; diff --git a/src/ssl_ech.c b/src/ssl_ech.c index eb9b9a6a5a..8b81c8ca6e 100644 --- a/src/ssl_ech.c +++ b/src/ssl_ech.c @@ -311,6 +311,25 @@ int wolfSSL_SetEchConfigs(WOLFSSL* ssl, const byte* echConfigs, return ret; } +/* store retry configs received during ECH rejection + * returns 0 on success, error otherwise */ +int SetRetryConfigs(WOLFSSL* ssl, const byte* echConfigs, word32 echConfigsLen) +{ + int ret; + + if (ssl == NULL || echConfigs == NULL || echConfigsLen == 0) + return BAD_FUNC_ARG; + + if (ssl->echRetryConfigs != NULL) { + return WOLFSSL_FATAL_ERROR; + } + + ret = SetEchConfigsEx(&ssl->echRetryConfigs, ssl->heap, echConfigs, + echConfigsLen); + + return ret; +} + /* get the raw ech config from our struct */ int GetEchConfig(WOLFSSL_EchConfig* config, byte* output, word32* outputLen) { @@ -434,6 +453,21 @@ int wolfSSL_GetEchConfigs(WOLFSSL* ssl, byte* output, word32* outputLen) return GetEchConfigsEx(ssl->echConfigs, output, outputLen); } +/* wrapper function to get retry configs + * a client should only call this after the 'wolfSSL_connect()' call fails + * returns error if retry configs were not received or were malformed */ +int wolfSSL_GetEchRetryConfigs(WOLFSSL* ssl, byte* output, word32* outputLen) +{ + if (ssl == NULL || outputLen == NULL) + return BAD_FUNC_ARG; + + if (ssl->echRetryConfigs == NULL || !ssl->options.echRetryConfigsAccepted) { + return WOLFSSL_FATAL_ERROR; + } + + return GetEchConfigsEx(ssl->echRetryConfigs, output, outputLen); +} + void wolfSSL_SetEchEnable(WOLFSSL* ssl, byte enable) { if (ssl != NULL) { @@ -446,10 +480,41 @@ void wolfSSL_SetEchEnable(WOLFSSL* ssl, byte enable) } } +/* Walk the ECHConfigExtension list and check for mandatory extensions. + * Returns: + * 0 if all extensions are known/optional, + * 1 if an unsupported mandatory extension (high bit set) is present, + * error otherwise. */ +static int EchConfigCheckExtensions(const byte* exts, word16 extsLen) +{ + word16 bytesLeft = extsLen; + word16 extType; + word16 extDataLen; + + while (bytesLeft >= 4) { + ato16(exts, &extType); + ato16(exts + 2, &extDataLen); + if (bytesLeft - 4 < extDataLen) + return BUFFER_E; + if (extType & 0x8000) + return 1; + exts += 4 + extDataLen; + bytesLeft -= 4 + extDataLen; + } + + if (bytesLeft != 0) + return BUFFER_E; + + return 0; +} + +/* Parse the ECH configs and output to the corresponding outputConfigs + * return 0 on success, error otherwise */ int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, const byte* echConfigs, word32 echConfigsLen) { int ret = 0; + int unsupportedAlgos = 0; word32 configIdx; word32 idx; int j; @@ -470,12 +535,13 @@ int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, /* check that the total length is well formed */ ato16(echConfigs, &totalLength); - if (totalLength != echConfigsLen - 2) { - return WOLFSSL_FATAL_ERROR; - } + if (totalLength != echConfigsLen - 2) + return BUFFER_E; + configIdx = 2; do { + /* version (2) + length (2) */ if (configIdx + 4 > echConfigsLen) { ret = BUFFER_E; break; @@ -484,90 +550,89 @@ int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, ato16(echConfig, &version); ato16(echConfig + 2, &length); - if (configIdx + length + 4 > echConfigsLen) { + if (configIdx + 4 + length > echConfigsLen) { ret = BUFFER_E; break; } - else if (version != TLSX_ECH) { - /* skip this config and try the next one */ - configIdx += length + 4; + if (version != TLSX_ECH) { + configIdx += 4 + length; continue; } if (workingConfig == NULL) { - workingConfig = - (WOLFSSL_EchConfig*)XMALLOC(sizeof(WOLFSSL_EchConfig), heap, - DYNAMIC_TYPE_TMP_BUFFER); + workingConfig = (WOLFSSL_EchConfig*)XMALLOC( + sizeof(WOLFSSL_EchConfig), heap, DYNAMIC_TYPE_TMP_BUFFER); configList = workingConfig; } else { lastConfig = workingConfig; - workingConfig->next = - (WOLFSSL_EchConfig*)XMALLOC(sizeof(WOLFSSL_EchConfig), heap, - DYNAMIC_TYPE_TMP_BUFFER); + workingConfig->next = (WOLFSSL_EchConfig*)XMALLOC( + sizeof(WOLFSSL_EchConfig), heap, DYNAMIC_TYPE_TMP_BUFFER); workingConfig = workingConfig->next; } - if (workingConfig == NULL) { ret = MEMORY_E; break; } - XMEMSET(workingConfig, 0, sizeof(WOLFSSL_EchConfig)); - /* rawLen */ - workingConfig->rawLen = length + 4; - - /* raw body */ + workingConfig->rawLen = 4 + length; workingConfig->raw = (byte*)XMALLOC(workingConfig->rawLen, heap, - DYNAMIC_TYPE_TMP_BUFFER); + DYNAMIC_TYPE_TMP_BUFFER); if (workingConfig->raw == NULL) { ret = MEMORY_E; break; } - XMEMCPY(workingConfig->raw, echConfig, workingConfig->rawLen); - /* skip over version and length */ + /* version and length already checked */ echConfig += 4; + idx = 0; - idx = 5; - if (idx >= length) { + /* configId */ + if (idx + 1 > length) { ret = BUFFER_E; break; } + workingConfig->configId = echConfig[idx]; + idx += 1; - /* configId, 1 byte */ - workingConfig->configId = *echConfig; - echConfig++; - /* kemId, 2 bytes */ - ato16(echConfig, &workingConfig->kemId); - echConfig += 2; - /* hpke public_key length, 2 bytes */ - ato16(echConfig, &hpkePubkeyLen); - echConfig += 2; + /* kemId */ + if (idx + 2 > length) { + ret = BUFFER_E; + break; + } + ato16(echConfig + idx, &workingConfig->kemId); + idx += 2; /* hpke public_key */ - if (hpkePubkeyLen > HPKE_Npk_MAX || hpkePubkeyLen == 0) { + if (idx + 2 > length) { ret = BUFFER_E; break; } - idx += hpkePubkeyLen; - if (idx >= length) { + ato16(echConfig + idx, &hpkePubkeyLen); + idx += 2; + if (idx + hpkePubkeyLen > length) { ret = BUFFER_E; break; } + /* unsupported KEM: skip pubkey; end of loop will free this config */ + if (wc_HpkeKemIsSupported(workingConfig->kemId)) { + if (hpkePubkeyLen != wc_HpkeKemGetEncLen(workingConfig->kemId)) { + ret = BUFFER_E; + break; + } + XMEMCPY(workingConfig->receiverPubkey, echConfig + idx, hpkePubkeyLen); + } + idx += hpkePubkeyLen; - XMEMCPY(workingConfig->receiverPubkey, echConfig, hpkePubkeyLen); - echConfig += hpkePubkeyLen; - - /* cipherSuitesLen */ - idx += 2; - if (idx >= length) { + /* cipher suites */ + if (idx + 2 > length) { ret = BUFFER_E; break; } - ato16(echConfig, &cipherSuitesLen); + ato16(echConfig + idx, &cipherSuitesLen); + idx += 2; if (cipherSuitesLen == 0 || cipherSuitesLen % 4 != 0 || cipherSuitesLen >= 1024) { /* numCipherSuites is a byte so only 256 ciphersuites (each 4 bytes) @@ -575,108 +640,92 @@ int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, ret = BUFFER_E; break; } - - idx += cipherSuitesLen; - if (idx >= length) { + if (idx + cipherSuitesLen > length) { ret = BUFFER_E; break; } - workingConfig->cipherSuites = (EchCipherSuite*)XMALLOC(cipherSuitesLen, heap, DYNAMIC_TYPE_TMP_BUFFER); if (workingConfig->cipherSuites == NULL) { ret = MEMORY_E; break; } - - echConfig += 2; workingConfig->numCipherSuites = (byte)(cipherSuitesLen / 4); - /* cipherSuites */ for (j = 0; j < workingConfig->numCipherSuites; j++) { - ato16(echConfig, &workingConfig->cipherSuites[j].kdfId); - ato16(echConfig + 2, &workingConfig->cipherSuites[j].aeadId); - echConfig += 4; + ato16(echConfig + idx, &workingConfig->cipherSuites[j].kdfId); + ato16(echConfig + idx + 2, &workingConfig->cipherSuites[j].aeadId); + idx += 4; } - /* ignore the maximum name length */ - idx++; - if (idx >= length) { + /* ignore maximum name length */ + if (idx + 1 > length) { ret = BUFFER_E; break; } - echConfig++; + idx += 1; - /* publicNameLen */ - idx++; - if (idx >= length) { + /* publicName */ + if (idx + 1 > length) { ret = BUFFER_E; break; } - - publicNameLen = *echConfig; + publicNameLen = echConfig[idx]; + idx += 1; if (publicNameLen == 0) { ret = BUFFER_E; break; } - - idx += publicNameLen; - if (idx >= length) { + if (idx + publicNameLen > length) { ret = BUFFER_E; break; } - echConfig++; - - workingConfig->publicName = (char*)XMALLOC(publicNameLen + 1, - heap, DYNAMIC_TYPE_TMP_BUFFER); + workingConfig->publicName = (char*)XMALLOC(publicNameLen + 1, heap, + DYNAMIC_TYPE_TMP_BUFFER); if (workingConfig->publicName == NULL) { ret = MEMORY_E; break; } - - /* publicName */ - XMEMCPY(workingConfig->publicName, echConfig, publicNameLen); + XMEMCPY(workingConfig->publicName, echConfig + idx, publicNameLen); workingConfig->publicName[publicNameLen] = '\0'; - echConfig += publicNameLen; + idx += publicNameLen; - /* TODO: Parse ECHConfigExtension */ - /* --> for now just ignore it */ - idx += 2; - if (idx > length) { + /* extensions */ + if (idx + 2 > length) { ret = BUFFER_E; break; } - ato16(echConfig, &extensionsLen); - - idx += extensionsLen; - if (idx != length) { + ato16(echConfig + idx, &extensionsLen); + idx += 2; + if (idx + extensionsLen != length) { ret = BUFFER_E; break; } - /* KEM or ciphersuite not supported, free this config and then try to - * parse another */ - if (EchConfigGetSupportedCipherSuite(workingConfig) < 0) { + ret = EchConfigCheckExtensions(echConfig + idx, extensionsLen); + if (ret < 0) + break; + + /* KEM, ciphersuite, or mandatory extension not supported, free this + * config and then try to parse another */ + if (ret > 0 || EchConfigGetSupportedCipherSuite(workingConfig) < 0) { + ret = 0; + unsupportedAlgos = 1; XFREE(workingConfig->cipherSuites, heap, DYNAMIC_TYPE_TMP_BUFFER); XFREE(workingConfig->publicName, heap, DYNAMIC_TYPE_TMP_BUFFER); XFREE(workingConfig->raw, heap, DYNAMIC_TYPE_TMP_BUFFER); XFREE(workingConfig, heap, DYNAMIC_TYPE_TMP_BUFFER); workingConfig = lastConfig; - - if (workingConfig != NULL) { + if (workingConfig != NULL) workingConfig->next = NULL; - } - else { - /* if one (or more) of the leading configs are unsupported then - * this case will be hit */ + else configList = NULL; - } } configIdx += 4 + length; } while (configIdx < echConfigsLen); - if (ret == 0 && configIdx != echConfigsLen){ + + if (ret == 0 && configIdx != echConfigsLen) ret = BUFFER_E; - } /* if we found valid configs */ if (ret == 0 && configList != NULL) { @@ -696,8 +745,11 @@ int SetEchConfigsEx(WOLFSSL_EchConfig** outputConfigs, void* heap, XFREE(lastConfig, heap, DYNAMIC_TYPE_TMP_BUFFER); } + /* syntactically correct but configs are not supported */ + if (ret == 0 && unsupportedAlgos) + return UNSUPPORTED_SUITE; if (ret == 0) - return WOLFSSL_FATAL_ERROR; + return UNSUPPORTED_PROTO_VERSION; return ret; } @@ -716,7 +768,6 @@ int GetEchConfigsEx(WOLFSSL_EchConfig* configs, byte* output, word32* outputLen) return BAD_FUNC_ARG; } - /* skip over total length which we fill in later */ if (output != NULL) { workingOutputLen = *outputLen - totalLen; diff --git a/src/tls.c b/src/tls.c index f8ba67db70..2fdc8b5808 100644 --- a/src/tls.c +++ b/src/tls.c @@ -13866,6 +13866,8 @@ static int TLSX_ECH_CheckInnerPadding(WOLFSSL* ssl, WOLFSSL_ECH* ech) headerSz = ssl->options.dtls ? DTLS13_HANDSHAKE_HEADER_SZ : HANDSHAKE_HEADER_SZ; #else + (void)ssl; + headerSz = HANDSHAKE_HEADER_SZ; #endif @@ -13905,7 +13907,6 @@ static int TLSX_ECH_CheckInnerPadding(WOLFSSL* ssl, WOLFSSL_ECH* ech) acc |= innerCh[i]; } if (acc != 0) { - SendAlert(ssl, alert_fatal, illegal_parameter); return INVALID_PARAMETER; } @@ -13990,14 +13991,14 @@ static const byte* TLSX_ECH_FindOuterExtension(const byte* outerCh, /* If newinnerCh is NULL, validate ordering and existence of references * - updates newInnerChLen with total length of selected extensions - * If newinnerCh in not NULL, copy extensions into newInnerCh + * If newinnerCh is not NULL, copy extensions into newInnerCh * * outerCh The outer ClientHello buffer. * outerChLen Outer ClientHello length. * newInnerCh The inner ClientHello buffer. * newInnerChLen Inner ClientHello length. * numOuterRefs Number of references described by OuterExtensions extension. - * numOuterTypes References described by OuterExtensions extension. + * OuterRefTypes References described by OuterExtensions extension. * returns 0 on success and otherwise failure. */ static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen, @@ -14008,55 +14009,43 @@ static int TLSX_ECH_CopyOuterExtensions(const byte* outerCh, word32 outerChLen, word16 refType; word32 outerExtLen; word32 outerExtOffset = 0; - word16 extsStart; - word16 extsLen; + word16 extsStart = 0; + word16 extsLen = 0; const byte* outerExtData; if (newInnerCh == NULL) { *newInnerChLen = 0; + } - while (numOuterRefs-- > 0) { - ato16(outerRefTypes, &refType); + while (numOuterRefs-- > 0) { + ato16(outerRefTypes, &refType); - if (refType == TLSXT_ECH) { - WOLFSSL_MSG("ECH: ech_outer_extensions references ECH"); - ret = INVALID_PARAMETER; - break; - } + if (refType == TLSXT_ECH) { + WOLFSSL_MSG("ECH: ech_outer_extensions references ECH"); + ret = INVALID_PARAMETER; + break; + } - outerExtData = TLSX_ECH_FindOuterExtension(outerCh, outerChLen, - refType, &outerExtLen, &outerExtOffset, - &extsStart, &extsLen); + outerExtData = TLSX_ECH_FindOuterExtension(outerCh, outerChLen, + refType, &outerExtLen, &outerExtOffset, + &extsStart, &extsLen); - if (outerExtData == NULL) { - WOLFSSL_MSG("ECH: referenced extension not in outer CH"); - ret = INVALID_PARAMETER; - break; - } + if (outerExtData == NULL) { + WOLFSSL_MSG("ECH: referenced extension not in outer CH or out " + "of order"); + ret = INVALID_PARAMETER; + break; + } + if (newInnerCh == NULL) { *newInnerChLen += outerExtLen; - - outerRefTypes += OPAQUE16_LEN; } - } - else { - while (numOuterRefs-- > 0) { - ato16(outerRefTypes, &refType); - - outerExtData = TLSX_ECH_FindOuterExtension(outerCh, outerChLen, - refType, &outerExtLen, &outerExtOffset, - &extsStart, &extsLen); - - if (outerExtData == NULL) { - ret = INVALID_PARAMETER; - break; - } - + else { XMEMCPY(*newInnerCh, outerExtData, outerExtLen); *newInnerCh += outerExtLen; - - outerRefTypes += OPAQUE16_LEN; } + + outerRefTypes += OPAQUE16_LEN; } return ret; @@ -14264,6 +14253,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, { int ret = 0; int i; + int allocatedHpke = 0; word32 rawConfigLen = 0; byte* info = NULL; word32 infoLen = 0; @@ -14284,6 +14274,7 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, } /* check if hpke already exists, may if HelloRetryRequest */ if (ech->hpke == NULL) { + allocatedHpke = 1; ech->hpke = (Hpke*)XMALLOC(sizeof(Hpke), heap, DYNAMIC_TYPE_TMP_BUFFER); if (ech->hpke == NULL) ret = MEMORY_E; @@ -14332,8 +14323,9 @@ static int TLSX_ExtractEch(WOLFSSL_ECH* ech, WOLFSSL_EchConfig* echConfig, ech->outerClientPayload, ech->innerClientHelloLen, ech->innerClientHello + HANDSHAKE_HEADER_SZ); } - /* free the hpke and context on failure */ - if (ret != 0) { + /* only free hpke/hpkeContext if allocated in this call; otherwise preserve + * them for clientHello2 */ + if (ret != 0 && allocatedHpke) { XFREE(ech->hpke, heap, DYNAMIC_TYPE_TMP_BUFFER); ech->hpke = NULL; XFREE(ech->hpkeContext, heap, DYNAMIC_TYPE_TMP_BUFFER); @@ -14371,16 +14363,36 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, /* retry configs */ if (msgType == encrypted_extensions) { - ret = wolfSSL_SetEchConfigs(ssl, readBuf, size); + /* configs must only be sent on ECH rejection (RFC9849, Section 5) */ + if (ssl->options.echAccepted) { + SendAlert(ssl, alert_fatal, unsupported_extension); + WOLFSSL_ERROR_VERBOSE(UNSUPPORTED_EXTENSION); + return UNSUPPORTED_EXTENSION; + } - if (ret == WOLFSSL_SUCCESS) + ret = SetRetryConfigs(ssl, readBuf, (word32)size); + if (ret == WC_NO_ERR_TRACE(UNSUPPORTED_SUITE) || + ret == WC_NO_ERR_TRACE(UNSUPPORTED_PROTO_VERSION)) { + WOLFSSL_MSG("ECH retry configs had 'bad version' or 'bad suite'"); ret = 0; + } + + if (ssl->echConfigs == NULL) { + /* on GREASE connection configs must be checked syntactically and + * must not be saved (RFC 9849, Section 6.2.1) */ + FreeEchConfigs(ssl->echRetryConfigs, ssl->heap); + ssl->echRetryConfigs = NULL; + } + + /* retry configs may only be accepted at the point when ECH_REQUIRED is + * sent */ + ssl->options.echRetryConfigsAccepted = 0; } /* HRR with special confirmation */ else if (msgType == hello_retry_request && ssl->echConfigs != NULL) { /* length must be 8 */ if (size != ECH_ACCEPT_CONFIRMATION_SZ) - return BAD_FUNC_ARG; + return BUFFER_ERROR; /* get extension */ echX = TLSX_Find(ssl->extensions, TLSX_ECH); @@ -14397,17 +14409,28 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, return BAD_FUNC_ARG; ech = (WOLFSSL_ECH*)echX->data; + /* if the first ECH was rejected or CH1 did not have ECH then there is + * no need to decrypt this one */ + if (!ssl->options.echAccepted && ssl->options.serverState == + SERVER_HELLO_RETRY_REQUEST_COMPLETE) { + ech->state = ECH_WRITE_RETRY_CONFIGS; + return 0; + } + /* read the ech parameters before the payload */ ech->type = *readBuf_p; readBuf_p++; offset += 1; - if (ech->type == ECH_TYPE_INNER) { + if (ssl->options.echProcessingInner && ech->type == ECH_TYPE_INNER) { ech->state = ECH_PARSED_INTERNAL; return 0; } - else if (ech->type != ECH_TYPE_OUTER) { - /* type MUST be INNER or OUTER */ - return BAD_FUNC_ARG; + else if ((!ssl->options.echProcessingInner && + ech->type != ECH_TYPE_OUTER) || + (ssl->options.echProcessingInner && + ech->type != ECH_TYPE_INNER)) { + /* MUST process INNER in inner hello and OUTER in outer hello */ + return INVALID_PARAMETER; } /* Must have kdfId, aeadId, configId, enc len and payload len. */ if (size < offset + 2 + 2 + 1 + 2 + 2) { @@ -14434,10 +14457,10 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, /* Check encLen isn't more than remaining bytes minus * payload length. */ if (len > size - offset - 2) { - return BAD_FUNC_ARG; + return BUFFER_ERROR; } if (len > HPKE_Npk_MAX) { - return BAD_FUNC_ARG; + return BUFFER_ERROR; } /* read enc */ XMEMCPY(ech->enc, readBuf_p, len); @@ -14448,27 +14471,27 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, /* kdfId */ ato16(readBuf_p, &tmpVal16); if (tmpVal16 != ech->cipherSuite.kdfId) { - return BAD_FUNC_ARG; + return INVALID_PARAMETER; } readBuf_p += 2; offset += 2; /* aeadId */ ato16(readBuf_p, &tmpVal16); if (tmpVal16 != ech->cipherSuite.aeadId) { - return BAD_FUNC_ARG; + return INVALID_PARAMETER; } readBuf_p += 2; offset += 2; /* configId */ if (*readBuf_p != ech->configId) { - return BAD_FUNC_ARG; + return INVALID_PARAMETER; } readBuf_p++; offset++; /* on an HRR the enc value MUST be empty */ ato16(readBuf_p, &len); if (len != 0) { - return BAD_FUNC_ARG; + return INVALID_PARAMETER; } readBuf_p += 2; offset += 2; @@ -14481,7 +14504,7 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, offset += 2; /* Check payload is no bigger than remaining bytes. */ if (ech->innerClientHelloLen > size - offset) { - return BAD_FUNC_ARG; + return BUFFER_ERROR; } if (ech->innerClientHelloLen < WC_AES_BLOCK_SIZE) { return BUFFER_ERROR; @@ -14529,7 +14552,25 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, echConfig = echConfig->next; } } - if (ret == 0) { + /* if we failed to extract/expand */ + if (ret != 0) { + WOLFSSL_MSG("Failed to decrypt InnerHello"); + if (ech->hpkeContext != NULL) { + /* on SH2 this is fatal */ + SendAlert(ssl, alert_fatal, decrypt_error); + WOLFSSL_ERROR_VERBOSE(DECRYPT_ERROR); + ret = DECRYPT_ERROR; + } + else { + /* on SH1 prepare to write retry configs */ + XFREE(ech->innerClientHello, ssl->heap, + DYNAMIC_TYPE_TMP_BUFFER); + ech->innerClientHello = NULL; + ech->state = ECH_WRITE_RETRY_CONFIGS; + ret = 0; + } + } + else { ret = TLSX_ECH_CheckInnerPadding(ssl, ech); if (ret == 0) { /* expand EchOuterExtensions if present. @@ -14537,14 +14578,11 @@ static int TLSX_ECH_Parse(WOLFSSL* ssl, const byte* readBuf, word16 size, ret = TLSX_ECH_ExpandOuterExtensions(ssl, ech, ssl->heap); } } - /* if we failed to extract/expand, set state to retry configs */ if (ret != 0) { XFREE(ech->innerClientHello, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); ech->innerClientHello = NULL; - ech->state = ECH_WRITE_RETRY_CONFIGS; } XFREE(aadCopy, ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); - return 0; } return ret; @@ -16283,6 +16321,12 @@ static int TLSX_GetSizeWithEch(WOLFSSL* ssl, byte* semaphore, byte msgType, WC_ALLOC_VAR_EX(serverName, char, WOLFSSL_HOST_NAME_MAX, NULL, DYNAMIC_TYPE_TMP_BUFFER, return MEMORY_E); r = TLSX_EchChangeSNI(ssl, &echX, serverName, &serverNameX, &extensions); + /* If ECH won't be written exclude it from the size calculation */ + if (r == 0 && echX != NULL && + !ssl->options.echAccepted && + ((WOLFSSL_ECH*)echX->data)->innerCount != 0) { + TURN_ON(semaphore, TLSX_ToSemaphore(echX->type)); + } if (r == 0 && ssl->extensions) ret = TLSX_GetSize(ssl->extensions, semaphore, msgType, pLength); if (r == 0 && ret == 0 && ssl->ctx && ssl->ctx->extensions) @@ -17834,6 +17878,12 @@ int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, byte msgType, WOLFSSL_MSG("ECH extension received"); ret = ECH_PARSE(ssl, input + offset, size, msgType); break; + case TLSXT_ECH_OUTER_EXTENSIONS: + /* RFC 9849 s5.1: ech_outer_extensions MUST only appear in + * the EncodedClientHelloInner */ + WOLFSSL_MSG("ech_outer_extensions in plaintext message"); + WOLFSSL_ERROR_VERBOSE(INVALID_PARAMETER); + return INVALID_PARAMETER; #endif default: WOLFSSL_MSG("Unknown TLS extension type"); @@ -17973,19 +18023,6 @@ int TLSX_Parse(WOLFSSL* ssl, const byte* input, word16 length, byte msgType, if (ret == 0) ret = TCA_VERIFY_PARSE(ssl, isRequest); -#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) - /* If client used ECH, server HRR must include ECH confirmation */ - if (ret == 0 && msgType == hello_retry_request && ssl->echConfigs != NULL && - !ssl->options.disableECH) { - TLSX* echX = TLSX_Find(ssl->extensions, TLSX_ECH); - if (echX == NULL || ((WOLFSSL_ECH*)echX->data)->confBuf == NULL) { - WOLFSSL_MSG("ECH used but HRR missing ECH confirmation"); - WOLFSSL_ERROR_VERBOSE(EXT_MISSING); - ret = EXT_MISSING; - } - } -#endif - WOLFSSL_LEAVE("Leaving TLSX_Parse", ret); return ret; } diff --git a/src/tls13.c b/src/tls13.c index fa7f46bdba..3685faf275 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -4899,7 +4899,8 @@ int SendTls13ClientHello(WOLFSSL* ssl) #if defined(HAVE_ECH) /* write inner then outer */ if (ssl->echConfigs != NULL && !ssl->options.disableECH && - (ssl->options.echAccepted || args->ech->innerCount == 0)) { + (ssl->options.echAccepted || args->ech->innerCount == 0)) { + byte downgrade; /* set the type to inner */ args->ech->type = ECH_TYPE_INNER; /* innerClientHello may already exist from hrr, free if it does */ @@ -4940,11 +4941,15 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* copy the new client random */ XMEMCPY(ssl->arrays->clientRandom, args->output + args->clientRandomOffset, RAN_LEN); - /* write the extensions for inner */ + /* write the extensions for inner + * ensuring that a version less than TLS1.3 is never offered */ args->length = 0; - ret = TLSX_WriteRequest(ssl, args->ech->innerClientHello + args->idx - - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ), client_hello, - &args->length); + downgrade = ssl->options.downgrade; + ssl->options.downgrade = 0; + ret = TLSX_WriteRequest(ssl, args->ech->innerClientHello + + args->idx - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ), + client_hello, &args->length); + ssl->options.downgrade = downgrade; if (ret != 0) return ret; /* set the type to outer */ @@ -4965,6 +4970,14 @@ int SendTls13ClientHello(WOLFSSL* ssl) /* encrypt and pack the ech innerClientHello */ if (ssl->echConfigs != NULL && !ssl->options.disableECH && (ssl->options.echAccepted || args->ech->innerCount == 0)) { +#if defined(WOLFSSL_TEST_ECH) + if (ssl->echInnerHelloCb != NULL) { + ret = ssl->echInnerHelloCb(args->ech->innerClientHello, + args->ech->innerClientHelloLen - args->ech->hpke->Nt); + if (ret != 0) + return ret; + } +#endif ret = TLSX_FinalizeEch(args->ech, args->output + RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ, (word32)(args->sendSz - (RECORD_HEADER_SZ + HANDSHAKE_HEADER_SZ))); @@ -5128,6 +5141,7 @@ static int EchCheckAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, ECH_ACCEPT_CONFIRMATION_SZ); if (ret == 0) { + WOLFSSL_MSG("ECH accepted"); ssl->options.echAccepted = 1; /* after HRR, hsHashesEch must contain: @@ -5144,8 +5158,18 @@ static int EchCheckAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, } } else { + WOLFSSL_MSG("ECH rejected"); + + if (msgType != hello_retry_request && ssl->options.echAccepted) { + /* the SH has rejected ECH after the HRR has accepted it + * RFC 9849, section 6.1.5 */ + WOLFSSL_MSG("ECH rejected, but it was previously accepted..."); + ret = INVALID_PARAMETER; + } + else { + ret = 0; + } ssl->options.echAccepted = 0; - ret = 0; /* ECH rejected, continue with outer transcript */ FreeHandshakeHashes(ssl); @@ -5701,31 +5725,40 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #if defined(HAVE_ECH) /* check for acceptConfirmation */ - if (ssl->echConfigs != NULL && !ssl->options.disableECH) { + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + ssl->hsHashesEch != NULL) { args->echX = TLSX_Find(ssl->extensions, TLSX_ECH); - /* account for hrr extension instead of server random */ - if (args->extMsgType == hello_retry_request) { - args->acceptOffset = - (word32)(((WOLFSSL_ECH*)args->echX->data)->confBuf - input); - args->acceptLabel = (byte*)echHrrAcceptConfirmationLabel; - args->acceptLabelSz = ECH_HRR_ACCEPT_CONFIRMATION_LABEL_SZ; + if (args->extMsgType == hello_retry_request && + ((WOLFSSL_ECH*)args->echX->data)->confBuf == NULL) { + /* server rejected ECH, fallback to outer */ + Free_HS_Hashes(ssl->hsHashesEch, ssl->heap); + ssl->hsHashesEch = NULL; } else { - args->acceptLabel = (byte*)echAcceptConfirmationLabel; - args->acceptLabelSz = ECH_ACCEPT_CONFIRMATION_LABEL_SZ; - } - /* check acceptance */ - if (ret == 0) { - ret = EchCheckAcceptance(ssl, args->acceptLabel, - args->acceptLabelSz, input, args->acceptOffset, helloSz, - args->extMsgType); - } - if (ret != 0) - return ret; - /* use the inner random for client random */ - if (args->extMsgType != hello_retry_request) { - XMEMCPY(ssl->arrays->clientRandom, ssl->arrays->clientRandomInner, - RAN_LEN); + /* account for hrr extension instead of server random */ + if (args->extMsgType == hello_retry_request) { + args->acceptOffset = + (word32)(((WOLFSSL_ECH*)args->echX->data)->confBuf - input); + args->acceptLabel = (byte*)echHrrAcceptConfirmationLabel; + args->acceptLabelSz = ECH_HRR_ACCEPT_CONFIRMATION_LABEL_SZ; + } + else { + args->acceptLabel = (byte*)echAcceptConfirmationLabel; + args->acceptLabelSz = ECH_ACCEPT_CONFIRMATION_LABEL_SZ; + } + /* check acceptance */ + if (ret == 0) { + ret = EchCheckAcceptance(ssl, args->acceptLabel, + args->acceptLabelSz, input, args->acceptOffset, helloSz, + args->extMsgType); + } + if (ret != 0) + return ret; + /* use the inner random for client random */ + if (args->extMsgType != hello_retry_request) { + XMEMCPY(ssl->arrays->clientRandom, + ssl->arrays->clientRandomInner, RAN_LEN); + } } } #endif /* HAVE_ECH */ @@ -5744,6 +5777,14 @@ int DoTls13ServerHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, XMEMSET(ssl->arrays->psk_key, 0, MAX_PSK_KEY_LEN); } else { +#if defined(HAVE_ECH) + /* do not resume when outerHandshake will be negotiated */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + WOLFSSL_MSG("ECH rejected but server negotiated PSK"); + return INVALID_PARAMETER; + } +#endif #ifdef WOLFSSL_CERT_WITH_EXTERN_PSK if (ssl->options.certWithExternPsk && psk->resumption) { /* RFC8773bis mode requires external PSK, not ticket resumption. */ @@ -5974,6 +6015,15 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, return ret; #endif +#if defined(HAVE_ECH) + /* RFC 9849 s6.1.7: ECH was offered but rejected by the server... + * the client MUST respond with an empty Certificate message. */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + ssl->options.sendVerify = SEND_BLANK_CERT; + } + else +#endif if ((ssl->buffers.certificate && ssl->buffers.certificate->buffer && ((ssl->buffers.key && ssl->buffers.key->buffer) #ifdef HAVE_PK_CALLBACKS @@ -7004,9 +7054,12 @@ static int EchWriteAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, output + acceptOffset); if (ret == 0) { + WOLFSSL_MSG("ECH accepted"); + tmpHashes = ssl->hsHashes; ssl->hsHashes = ssl->hsHashesEch; + ssl->options.echAccepted = 1; /* after HRR, hsHashesEch must contain: * message_hash(ClientHelloInner1) || HRR (actual, not zeros) */ if (msgType == hello_retry_request) { @@ -7014,8 +7067,6 @@ static int EchWriteAcceptance(WOLFSSL* ssl, byte* label, word16 labelSz, } /* normal TLS code will calculate transcript of ServerHello */ else { - ssl->options.echAccepted = 1; - ssl->hsHashes = tmpHashes; FreeHandshakeHashes(ssl); tmpHashes = ssl->hsHashesEch; @@ -7221,6 +7272,15 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, if (wantDowngrade) { #ifndef WOLFSSL_NO_TLS12 byte realMinor; +#endif +#if defined(HAVE_ECH) + if (ssl->options.echProcessingInner) { + WOLFSSL_MSG("ECH: inner client hello does not support version " + "less than TLS v1.3"); + ERROR_OUT(INVALID_PARAMETER, exit_dch); + } +#endif +#ifndef WOLFSSL_NO_TLS12 if (!ssl->options.downgrade) { WOLFSSL_MSG("Client trying to connect with lesser version than " "TLS v1.3"); @@ -7379,13 +7439,23 @@ int DoTls13ClientHello(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } #if defined(HAVE_ECH) - if (echX != NULL && ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { + if (!ssl->options.echProcessingInner && echX != NULL && + ((WOLFSSL_ECH*)echX->data)->state == ECH_WRITE_NONE) { if (((WOLFSSL_ECH*)echX->data)->innerClientHello != NULL) { /* Client sent real ECH and inner hello was decrypted, jump to * exit so the caller can re-invoke with the inner hello */ goto exit_dch; } else { + /* If ECH was accepted in ClientHello1 then ClientHello2 MUST + * contain an ECH extension */ + if (ssl->options.serverState == + SERVER_HELLO_RETRY_REQUEST_COMPLETE && + ssl->options.echAccepted) { + WOLFSSL_MSG("Client did not send an EncryptedClientHello " + "extension"); + ERROR_OUT(INCOMPLETE_DATA, exit_dch); + } /* Server has ECH but client did not send ECH. Clear the * response flag so the empty ECH extension is not written * in EncryptedExtensions. */ @@ -7967,6 +8037,10 @@ int SendTls13ServerHello(WOLFSSL* ssl, byte extMsgType) if (extMsgType == hello_retry_request) { /* reset the ech state for round 2 */ ((WOLFSSL_ECH*)echX->data)->state = ECH_WRITE_NONE; + /* inner hello no longer needed, free it */ + XFREE(((WOLFSSL_ECH*)echX->data)->innerClientHello, + ssl->heap, DYNAMIC_TYPE_TMP_BUFFER); + ((WOLFSSL_ECH*)echX->data)->innerClientHello = NULL; } else { if (ret == 0) { @@ -12290,6 +12364,15 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, WOLFSSL_START(WC_FUNC_NEW_SESSION_TICKET_DO); WOLFSSL_ENTER("DoTls13NewSessionTicket"); +#ifdef HAVE_ECH + /* ignore session ticket when ECH is rejected */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + *inOutIdx += size + ssl->keys.padSz; + return 0; + } +#endif + /* Lifetime hint. */ if ((*inOutIdx - begin) + SESSION_HINT_SZ > size) return BUFFER_ERROR; @@ -13418,15 +13501,24 @@ int DoTls13HandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, *inOutIdx = echInOutIdx; /* call again with the inner hello */ if (ret == 0) { - ((WOLFSSL_ECH*)echX->data)->sniState = ECH_INNER_SNI; + if (((WOLFSSL_ECH*)echX->data)->sniState == ECH_OUTER_SNI) { + ((WOLFSSL_ECH*)echX->data)->sniState = ECH_INNER_SNI; + } + ssl->options.echProcessingInner = 1; ret = DoTls13ClientHello(ssl, ((WOLFSSL_ECH*)echX->data)->innerClientHello, &echInOutIdx, ((WOLFSSL_ECH*)echX->data)->innerClientHelloLen); + ssl->options.echProcessingInner = 0; ((WOLFSSL_ECH*)echX->data)->sniState = ECH_SNI_DONE; } + if (ret == 0 && ((WOLFSSL_ECH*)echX->data)->state != + ECH_PARSED_INTERNAL) { + WOLFSSL_MSG("ECH: inner ClientHello missing ECH extension"); + ret = INVALID_PARAMETER; + } /* if the inner ech parsed successfully we have successfully * handled the hello and can skip the whole message */ if (ret == 0) { @@ -14211,6 +14303,21 @@ int wolfSSL_connect_TLSv13(WOLFSSL* ssl) } #endif /* NO_HANDSHAKE_DONE_CB */ + #if defined(HAVE_ECH) + /* RFC 9849 s6.1.6: if we offered ECH but the server rejected it, + * send ech_required alert and abort before returning to the app */ + if (ssl->echConfigs != NULL && !ssl->options.disableECH && + !ssl->options.echAccepted) { + if (ssl->echRetryConfigs != NULL) { + ssl->options.echRetryConfigsAccepted = 1; + } + SendAlert(ssl, alert_fatal, ech_required); + ssl->error = ECH_REQUIRED_E; + WOLFSSL_ERROR_VERBOSE(ECH_REQUIRED_E); + return WOLFSSL_FATAL_ERROR; + } + #endif /* HAVE_ECH */ + if (!ssl->options.keepResources) { FreeHandshakeResources(ssl); } diff --git a/tests/api.c b/tests/api.c index 7cde2a6bdc..3c07cccdba 100644 --- a/tests/api.c +++ b/tests/api.c @@ -14383,10 +14383,18 @@ static int test_wolfSSL_Tls13_ECH_params_b64(void) #if !defined(NO_WOLFSSL_CLIENT) /* base64 ech configs from cloudflare-ech.com (these are good configs) */ const char* b64Valid = "AEX+DQBBFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="; + /* ech configs with bad version */ + const char* b64BadVers = "AEX+/gBBFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="; /* ech configs with bad/unsupported algorithm */ const char* b64BadAlgo = "AEX+DQBBFP7+ACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAA="; + /* ech configs with 0 length algorithm */ + const char* b64BadAlgo0 = "ACX+DQAhFAAgAAAABAABAAEAEmNsb3VkZmxhcmUtZWNoLmNvbQAA"; + /* ech configs with long algorithm */ + const char* b64BadAlgo33 = "AEb+DQBCFAAgACEBbgKECPPpYhFWEG1ygQ3lYE5hdq317ijtpJ4cKze44E4ABAABAAEAEmNsb3VkZmxhcmUtZWNoLmNvbQAA"; /* ech configs with bad/unsupported ciphersuite */ const char* b64BadCiph = "AEX+DQBBFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAE/v4AAQASY2xvdWRmbGFyZS1lY2guY29tAAA="; + /* ech configs with unrecognized mandatory extension */ + const char* b64Mandatory = "AEn+DQBFFAAgACBuAoQI8+liEVYQbXKBDeVgTmF2rfXuKO2knhwrN7jgTgAEAAEAAQASY2xvdWRmbGFyZS1lY2guY29tAAT6+gAA"; /* ech configs with bad version first */ const char* b64BadVers1 = "AIz+HQBCAQAgACCjR6+Qn9UYkMaWdXZzsby88vXFhPHJ2tWCDHQJLvMkEgAEAAEAAQATZWNoLXB1YmxpYy1uYW1lLmNvbQAA/g0AQgIAIAAgMM6vLrTbOfsfA6fTbJY/Iu0Lj2xeHEPGUJeUwQGAYF4ABAABAAEAE2VjaC1wdWJsaWMtbmFtZS5jb20AAA=="; /* ech configs with bad version second */ @@ -14418,18 +14426,42 @@ static int test_wolfSSL_Tls13_ECH_params_b64(void) ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, b64Valid, 0)); + /* bad version */ + ExpectIntEQ(UNSUPPORTED_PROTO_VERSION, wolfSSL_CTX_SetEchConfigsBase64(ctx, + b64BadVers, (word32)XSTRLEN(b64BadVers))); + ExpectIntEQ(UNSUPPORTED_PROTO_VERSION, wolfSSL_SetEchConfigsBase64(ssl, + b64BadVers, (word32)XSTRLEN(b64BadVers))); + /* bad algorithm */ - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, + ExpectIntEQ(UNSUPPORTED_SUITE, wolfSSL_CTX_SetEchConfigsBase64(ctx, b64BadAlgo, (word32)XSTRLEN(b64BadAlgo))); - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, + ExpectIntEQ(UNSUPPORTED_SUITE, wolfSSL_SetEchConfigsBase64(ssl, b64BadAlgo, (word32)XSTRLEN(b64BadAlgo))); + /* bad algorithm with 0 length key */ + ExpectIntEQ(BUFFER_E, wolfSSL_CTX_SetEchConfigsBase64(ctx, + b64BadAlgo0, (word32)XSTRLEN(b64BadAlgo0))); + ExpectIntEQ(BUFFER_E, wolfSSL_SetEchConfigsBase64(ssl, + b64BadAlgo0, (word32)XSTRLEN(b64BadAlgo0))); + + /* bad algorithm with long key */ + ExpectIntEQ(BUFFER_E, wolfSSL_CTX_SetEchConfigsBase64(ctx, + b64BadAlgo33, (word32)XSTRLEN(b64BadAlgo33))); + ExpectIntEQ(BUFFER_E, wolfSSL_SetEchConfigsBase64(ssl, + b64BadAlgo33, (word32)XSTRLEN(b64BadAlgo33))); + /* bad ciphersuite */ - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, + ExpectIntEQ(UNSUPPORTED_SUITE, wolfSSL_CTX_SetEchConfigsBase64(ctx, b64BadCiph, (word32)XSTRLEN(b64BadCiph))); - ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, + ExpectIntEQ(UNSUPPORTED_SUITE, wolfSSL_SetEchConfigsBase64(ssl, b64BadCiph, (word32)XSTRLEN(b64BadCiph))); + /* unrecognized mandatory extension */ + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, + b64Mandatory, (word32)XSTRLEN(b64Mandatory))); + ExpectIntNE(WOLFSSL_SUCCESS, wolfSSL_SetEchConfigsBase64(ssl, + b64Mandatory, (word32)XSTRLEN(b64Mandatory))); + /* bad version first, should only have config 2 set */ ExpectIntEQ(WOLFSSL_SUCCESS, wolfSSL_CTX_SetEchConfigsBase64(ctx, b64BadVers1, (word32)XSTRLEN(b64BadVers1))); @@ -14588,22 +14620,6 @@ static int test_wolfSSL_Tls13_ECH_HRR(void) return test_wolfSSL_ECH_conn_ex(wolfTLSv1_3_server_method, wolfTLSv1_3_client_method, 1); } - -static int test_wolfSSL_SubTls13_ECH(void) -{ - EXPECT_DECLS; - -#ifndef WOLFSSL_NO_TLS12 - ExpectIntNE(test_wolfSSL_ECH_conn_ex(wolfTLSv1_3_server_method, - wolfTLSv1_2_client_method, 0), WOLFSSL_SUCCESS); - ExpectIntNE(test_wolfSSL_ECH_conn_ex(wolfTLSv1_2_server_method, - wolfTLSv1_3_client_method, 0), WOLFSSL_SUCCESS); - ExpectIntNE(test_wolfSSL_ECH_conn_ex(wolfSSLv23_server_method, - wolfTLSv1_2_client_method, 0), WOLFSSL_SUCCESS); -#endif - - return EXPECT_RESULT(); -} #endif /* HAVE_IO_TESTS_DEPENDENCIES */ #ifdef HAVE_SSL_MEMIO_TESTS_DEPENDENCIES @@ -14611,7 +14627,7 @@ static int test_wolfSSL_SubTls13_ECH(void) /* Static storage for passing ECH config between server and client callbacks */ static byte echCbTestConfigs[512]; static word32 echCbTestConfigsLen; -static const char* echCbTestPublicName = "ech-public-name.com"; +static const char* echCbTestPublicName = "example.com"; static const char* echCbTestPrivateName = "ech-private-name.com"; static word16 echCbTestKemID = 0; static word16 echCbTestKdfID = 0; @@ -14906,6 +14922,10 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), WOLFSSL_SUCCESS); + /* client will send empty cert on rejection, so server should not ask for + * cert */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + if (hrr) { ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); } @@ -14938,6 +14958,10 @@ static int test_wolfSSL_Tls13_ECH_bad_configs_ex(int hrr, int sniCb) ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, badPrivateName, (word16)XSTRLEN(badPrivateName)), WOLFSSL_SUCCESS); + /* client will send empty cert on rejection, so server should not ask for + * cert */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + if (hrr) { ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); } @@ -14966,6 +14990,221 @@ static int test_wolfSSL_Tls13_ECH_bad_configs(void) return EXPECT_RESULT(); } +/* Test retry configs are returned after ECH rejection and are usable */ +static int test_wolfSSL_Tls13_ECH_retry_configs_ex(int hrr) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + WOLFSSL_CTX* tempCtx = NULL; + byte badConfigs[256]; + word32 badConfigsLen = sizeof(badConfigs); + byte retryConfigs[256]; + word32 retryConfigsLen = sizeof(retryConfigs); + WOLFSSL_CTX* savedSCtx; + + /* --- first attempt: wrong ECH config -> ECH rejected --- */ + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* throwaway ECH config the server won't recognise */ + ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, echCbTestPublicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(tempCtx, badConfigs, &badConfigsLen), + WOLFSSL_SUCCESS); + wolfSSL_CTX_free(tempCtx); + tempCtx = NULL; + + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfigs, + badConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + + if (hrr) + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* ECH must fail and retry configs must be present */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + /* capture retry configs */ + ExpectIntEQ(wolfSSL_GetEchRetryConfigs(test_ctx.c_ssl, retryConfigs, + &retryConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->error, ECH_REQUIRED_E); + ExpectIntGT(retryConfigsLen, 0); + + if (EXPECT_SUCCESS()){ + /* keep server CTX so second attempt uses the same ECH keys */ + savedSCtx = test_ctx.s_ctx; + test_ctx.s_cb.isSharedCtx = 1; + test_ssl_memio_cleanup(&test_ctx); + + /* --- second attempt: same server CTX, retry configs -> accepted --- */ + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + /* restore server CTX; ctx_ready left NULL to skip ECH key regen */ + test_ctx.s_ctx = savedSCtx; + test_ctx.s_cb.isSharedCtx = 1; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, retryConfigs, + retryConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + + if (hrr) + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), + TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 1); + + wolfSSL_CTX_free(test_ctx.s_ctx); + test_ctx.s_ctx = NULL; + } + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_retry_configs(void) +{ + EXPECT_DECLS; + + ExpectIntEQ(test_wolfSSL_Tls13_ECH_retry_configs_ex(0), TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_retry_configs_ex(1), TEST_SUCCESS); + + return EXPECT_RESULT(); +} + +/* Test retry configs are cleared when authentication fails */ +static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(int hrr) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + WOLFSSL_CTX* tempCtx = NULL; + byte badConfigs[256]; + word32 badConfigsLen = sizeof(badConfigs); + word32 retryConfigsLen = sizeof(badConfigs); + const char* badPublicName = "ech-public-name.com"; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* generate mismatched ECH configs so retry_configs are sent + * and use a bad public name so auth fails in outer hello */ + ExpectNotNull(tempCtx = wolfSSL_CTX_new(wolfTLSv1_3_server_method())); + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(tempCtx, badPublicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(tempCtx, badConfigs, &badConfigsLen), + WOLFSSL_SUCCESS); + wolfSSL_CTX_free(tempCtx); + tempCtx = NULL; + + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, badConfigs, + badConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPrivateName, (word16)XSTRLEN(echCbTestPrivateName)), + WOLFSSL_SUCCESS); + + /* Do not require client cert on server so it does not send + * CertificateRequest */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* use badPublicName so ECH public name matches */ + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + badPublicName, (word16)XSTRLEN(badPublicName)), + WOLFSSL_SUCCESS); + + if (hrr) + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* auth failure in outer handshake, not ech_required */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(DOMAIN_NAME_MISMATCH)); + + /* retry configs must not be accessible */ + ExpectIntNE(wolfSSL_GetEchRetryConfigs(test_ctx.c_ssl, NULL, + &retryConfigsLen), WOLFSSL_SUCCESS); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_retry_configs_auth_fail(void) +{ + EXPECT_DECLS; + + ExpectIntEQ(test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(0), + TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_retry_configs_auth_fail_ex(1), + TEST_SUCCESS); + + return EXPECT_RESULT(); +} + +/* Test that bad retry configs (unsupported cipher suite) from the server are + * ignored rather than propagating an error */ +static int test_wolfSSL_Tls13_ECH_retry_configs_bad(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + word32 retryConfigsLen = sizeof(echCbTestConfigs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* corrupt the server's cipher suite -> ECH decrypt will fail, and retry + * configs will have an unsupported KDF/AEAD pair. + * This will trigger the UNSUPPORTED_SUITE path in TLSX_ECH_Parse */ + if (EXPECT_SUCCESS() && test_ctx.s_ctx->echConfigs != NULL && + test_ctx.s_ctx->echConfigs->cipherSuites != NULL) { + test_ctx.s_ctx->echConfigs->cipherSuites[0].aeadId = 0xFEFE; + } + + /* bad retry configs are discarded - failure must be ECH_REQUIRED_E, + * not a retry-config parse error */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + /* no retry configs should be stored since they were all unsupported */ + ExpectIntNE(wolfSSL_GetEchRetryConfigs(test_ctx.c_ssl, NULL, + &retryConfigsLen), WOLFSSL_SUCCESS); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + /* Test that client info can be successfully decoded from one of multiple server * ECH configs * In this case the server is expected to try it's first config, fail, then try @@ -15047,15 +15286,15 @@ static int test_wolfSSL_Tls13_ECH_new_config(void) /* Test GREASE ECH: * 1. client sends GREASE ECH extension but server has no ECH configs so it - * ignores it, handshake succeeds normally, no ECH configs received + * ignores it, handshake succeeds normally * 2. client sends GREASE ECH extensions and server has ECH configs, handshake - * succeeds and client receives ECH configs */ + * succeeds + * configs should never be received */ static int test_wolfSSL_Tls13_ECH_GREASE(void) { EXPECT_DECLS; test_ssl_memio_ctx test_ctx; - byte greaseConfigs[512]; - word32 greaseConfigsLen = sizeof(greaseConfigs); + word32 retryConfigsLen = sizeof(test_ctx); /* GREASE when server has no ECH configs */ @@ -15075,7 +15314,7 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) ExpectIntEQ(test_ctx.c_ssl->options.disableECH, 0); /* verify no ECH configs are set */ ExpectNull(test_ctx.s_ctx->echConfigs); - ExpectNull(test_ctx.c_ctx->echConfigs); + ExpectNull(test_ctx.c_ssl->echConfigs); /* handshake should succeed - server ignores the GREASE ECH extension */ ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); @@ -15083,8 +15322,11 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) /* ECH should NOT be accepted since this was GREASE */ ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 0); ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); - ExpectIntNE(wolfSSL_GetEchConfigs(test_ctx.c_ssl, greaseConfigs, - &greaseConfigsLen), WOLFSSL_SUCCESS); + /* verify no ECH configs are received */ + ExpectNull(test_ctx.c_ssl->echConfigs); + /* retry configs must not be saved */ + ExpectIntNE(wolfSSL_GetEchRetryConfigs(test_ctx.c_ssl, NULL, + &retryConfigsLen), WOLFSSL_SUCCESS); test_ssl_memio_cleanup(&test_ctx); @@ -15110,17 +15352,19 @@ static int test_wolfSSL_Tls13_ECH_GREASE(void) ExpectIntEQ(test_ctx.c_ssl->options.disableECH, 0); /* verify ECH configs are set on server */ ExpectNotNull(test_ctx.s_ctx->echConfigs); - ExpectNull(test_ctx.c_ctx->echConfigs); + ExpectNull(test_ctx.c_ssl->echConfigs); /* handshake should succeed - server responds to the GREASE ECH extension */ ExpectIntEQ(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); - /* ECH should NOT be accepted since this was GREASE - * However, configs will be present this time */ + /* ECH should NOT be accepted since this was GREASE */ ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 0); ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); - ExpectIntEQ(wolfSSL_GetEchConfigs(test_ctx.c_ssl, greaseConfigs, - &greaseConfigsLen), WOLFSSL_SUCCESS); + /* verify no ECH configs are received */ + ExpectNull(test_ctx.c_ssl->echConfigs); + /* retry configs must not be saved */ + ExpectIntNE(wolfSSL_GetEchRetryConfigs(test_ctx.c_ssl, NULL, + &retryConfigsLen), WOLFSSL_SUCCESS); test_ssl_memio_cleanup(&test_ctx); @@ -15188,6 +15432,7 @@ static int test_wolfSSL_Tls13_ECH_disable_conn(void) return EXPECT_RESULT(); } + /* Regression test: an inner SNI hostname >= MAX_PUBLIC_NAME_SZ (256) bytes * must not cause a stack-buffer-overflow in TLSX_EchRestoreSNI. Before the * fix, the truncated copy omitted the NUL terminator and XSTRLEN read past @@ -15233,6 +15478,297 @@ static int test_wolfSSL_Tls13_ECH_long_SNI(void) return EXPECT_RESULT(); } + +/* Test the HRR ECH rejection fallback path: + * client offers ECH, HRR is triggered, server sends HRR without ECH extension, + * client falls back to the outer transcript, then aborts with ech_required. */ +static int test_wolfSSL_Tls13_ECH_HRR_rejection(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + /* Server generates ECH config with good public name */ + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + /* Client sets the correct ECH config and private SNI */ + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Server must not require a client certificate */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* Disable ECH on the server SSL object: the server ignores ECH in CH1 and + * sends HRR without an ECH extension (confBuf stays NULL on the client) */ + wolfSSL_SetEchEnable(test_ctx.s_ssl, 0); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + echCbTestPublicName, (word16)XSTRLEN(echCbTestPublicName)), + WOLFSSL_SUCCESS); + + /* Force HRR so client receives HRR with no ECH extension, + * detects confBuf == NULL and frees hsHashesEch, falling back to the + * outer transcript */ + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* Handshake must fail: client aborts with ech_required */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + /* hsHashesEch must have been freed by the HRR rejection code path */ + ExpectNull(test_ctx.c_ssl->hsHashesEch); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +/* verify the server aborts if CH2 omits the ECH extension after the server + * accepted ECH in the HRR round */ +static int test_wolfSSL_Tls13_ECH_ch2_no_ech(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* withhold key shares from CH1 so the server is forced to send HRR */ + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* one round: client sends CH1, server processes it and sends HRR */ + (void)test_ssl_memio_do_handshake(&test_ctx, 1, NULL); + + /* server must have committed to ECH acceptance in the HRR */ + ExpectIntEQ(test_ctx.s_ssl->options.serverState, + SERVER_HELLO_RETRY_REQUEST_COMPLETE); + ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1); + + /* disable ECH on the client so CH2 omits the ECH extension entirely */ + wolfSSL_SetEchEnable(test_ctx.c_ssl, 0); + + /* rest of handshake must fail */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(INCOMPLETE_DATA)); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +/* verify that a decryption failure in CH2 is caught + * this also verifies that HPKE context is correctly reused */ +static int test_wolfSSL_Tls13_ECH_ch2_decrypt_error(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + int i; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Withhold key shares so the server is forced to send HRR */ + ExpectIntEQ(wolfSSL_NoKeyShares(test_ctx.c_ssl), WOLFSSL_SUCCESS); + + /* One round: client sends CH1, server processes it and sends HRR */ + (void)test_ssl_memio_do_handshake(&test_ctx, 1, NULL); + + ExpectIntEQ(test_ctx.s_ssl->options.serverState, + SERVER_HELLO_RETRY_REQUEST_COMPLETE); + ExpectIntEQ(test_ctx.s_ssl->options.echAccepted, 1); + + if (EXPECT_SUCCESS()) { + /* Client reads HRR and writes CH2 into s_buff */ + (void)wolfSSL_connect(test_ctx.c_ssl); + + /* Corrupt one byte of the ECH ciphertext in the CH2 record in s_buff. + * ECH outer extension layout after the 0xFE0D type marker: + * extLen(2) + outerType(1) + kdfId(2) + aeadId(2) + configId(1) + * + encLen(2, always 0 in CH2) + payloadLen(2) = 12 bytes, so the + * ciphertext starts 14 bytes past the first 0xFE byte. */ + for (i = 0; i < test_ctx.s_len - 1; i++) { + if (test_ctx.s_buff[i] == 0xFE && test_ctx.s_buff[i + 1] == 0x0D) { + if (i + 14 < test_ctx.s_len) + test_ctx.s_buff[i + 14] ^= 0xFF; + break; + } + } + + /* Server processes the corrupted CH2. + * hpkeContext is preserved, TLSX_ECH_Parse correctly identifies the CH2 + * round and sends decrypt_error. */ + (void)wolfSSL_accept(test_ctx.s_ssl); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(DECRYPT_ERROR)); + } + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +/* when ECH is rejected the certificate must match the public name of the chosen + * ech config + * the cert check should pass and the client aborts with ech_required */ +static int test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex(const char* publicName, + int validName) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + byte echConfigs[512]; + word32 echConfigsLen = sizeof(echConfigs); + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Generate ECH config with given public_name */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, publicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(test_ctx.s_ctx, echConfigs, + &echConfigsLen), WOLFSSL_SUCCESS); + + /* Client loads ECH configs and sets a private SNI */ + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echConfigs, + echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + "ech-private.com", (word16)XSTRLEN("ech-private.com")), + WOLFSSL_SUCCESS); + + /* Do not require client cert on server so it does not send + * CertificateRequest */ + wolfSSL_set_verify(test_ctx.s_ssl, WOLFSSL_VERIFY_NONE, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* Disable ECH on the server side so ECH is rejected */ + wolfSSL_SetEchEnable(test_ctx.s_ssl, 0); + + /* Match the server SNI to the ECH public_name */ + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + publicName, (word16)XSTRLEN(publicName)), WOLFSSL_SUCCESS); + + /* client sends ECH but server can't process it, however it is possible to + * fall back to the outer handshake */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + + if (validName) { + /* the server should see the handshake as successful + * the client should abort because the server did not use ECH */ + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(WOLFSSL_ERROR_NONE)); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + } + else { + /* the client should abort with cert mismatch + * the server error is then dependent on whether that cert mismatch + * results in an abort */ + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(DOMAIN_NAME_MISMATCH)); + } + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_rejected_cert_valid(void) +{ + EXPECT_DECLS; + + /* "example.com" appears in the SAN of certs/server-cert.pem */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("example.com", 1), + TEST_SUCCESS); + ExpectIntEQ(test_wolfSSL_Tls13_ECH_rejected_cert_valid_ex("badname.com", 0), + TEST_SUCCESS); + + return EXPECT_RESULT(); +} + +/* when ECH is rejected and the server requests a client certificate the client + * must respond with an empty cert */ +static int test_wolfSSL_Tls13_ECH_rejected_empty_client_cert(void) +{ + EXPECT_DECLS; + test_ssl_memio_ctx test_ctx; + byte echConfigs[512]; + word32 echConfigsLen = sizeof(echConfigs); + const char* publicName = "example.com"; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + test_ctx.s_cb.method = wolfTLSv1_3_server_method; + test_ctx.c_cb.method = wolfTLSv1_3_client_method; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* Generate ECH config with public_name matching the server cert SAN */ + ExpectIntEQ(wolfSSL_CTX_GenerateEchConfig(test_ctx.s_ctx, publicName, + 0, 0, 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_GetEchConfigs(test_ctx.s_ctx, echConfigs, + &echConfigsLen), WOLFSSL_SUCCESS); + + /* Client loads ECH configs and sets a private SNI */ + ExpectIntEQ(wolfSSL_SetEchConfigs(test_ctx.c_ssl, echConfigs, + echConfigsLen), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.c_ssl, WOLFSSL_SNI_HOST_NAME, + "ech-private.com", (word16)XSTRLEN("ech-private.com")), + WOLFSSL_SUCCESS); + + wolfSSL_set_verify(test_ctx.s_ssl, + WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + wolfSSL_set_verify(test_ctx.c_ssl, WOLFSSL_VERIFY_PEER, NULL); + + /* Disable ECH on the server so ECH is rejected */ + wolfSSL_SetEchEnable(test_ctx.s_ssl, 0); + + /* Match the Server SNI to the ECH public_name */ + ExpectIntEQ(wolfSSL_UseSNI(test_ctx.s_ssl, WOLFSSL_SNI_HOST_NAME, + publicName, (word16)XSTRLEN(publicName)), WOLFSSL_SUCCESS); + + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(test_ctx.c_ssl->options.echAccepted, 0); + + /* Server cert is valid for public_name, cert check passes, ech_required + * is sent on the client side. */ + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(ECH_REQUIRED_E)); + + /* The server received an empty Certificate from the client. + * With FAIL_IF_NO_PEER_CERT set, the server aborts with NO_PEER_CERT. */ + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(NO_PEER_CERT)); + + test_ssl_memio_cleanup(&test_ctx); + + return EXPECT_RESULT(); +} #endif /* HAVE_SSL_MEMIO_TESTS_DEPENDENCIES */ /* verify that ECH can be enabled/disabled without issue */ @@ -15286,6 +15822,260 @@ static int test_wolfSSL_Tls13_ECH_enable_disable(void) return EXPECT_RESULT(); } +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && \ + defined(WOLFSSL_TEST_ECH) && defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && \ + !defined(WOLFSSL_NO_TLS12) +static int ech_tamper_seek_extension(byte* innerCh, word16* innerExtLen) +{ + word16 idx; + byte sessionIdLen; + word16 cipherSuitesLen; + byte compressionLen; + + idx = OPAQUE16_LEN + RAN_LEN; + + sessionIdLen = innerCh[idx++]; + idx += sessionIdLen; + + ato16(innerCh + idx, &cipherSuitesLen); + idx += OPAQUE16_LEN + cipherSuitesLen; + + compressionLen = innerCh[idx++]; + idx += compressionLen; + + ato16(innerCh + idx, innerExtLen); + idx += OPAQUE16_LEN; + + return idx; +} + +static int ech_tamper_find_extension(byte* innerCh, word16* idx_p, + word16 extType) +{ + word16 idx; + word16 innerExtIdx; + word16 innerExtLen; + + idx = innerExtIdx = ech_tamper_seek_extension(innerCh, &innerExtLen); + + while (idx - innerExtIdx < innerExtLen) { + word16 type; + word16 len; + + ato16(innerCh + idx, &type); + if (type == extType) { + *idx_p = idx; + return 0; + } + + idx += OPAQUE16_LEN; + ato16(innerCh + idx, &len); + idx += OPAQUE16_LEN + len; + } + + return BAD_FUNC_ARG; +} + +static int ech_tamper_downgrade(byte* innerCh, word32 innerChLen) +{ + int ret; + word16 idx; + + (void)innerChLen; + + ret = ech_tamper_find_extension(innerCh, &idx, TLSXT_SUPPORTED_VERSIONS); + if (ret == 0) { + /* change extension type to something unknown */ + innerCh[idx] = 0xFA; + innerCh[idx + 1] = 0xFA; + return 0; + } + else { + return ret; + } +} + +static int ech_tamper_padding(byte* innerCh, word32 innerChLen) +{ + word16 idx; + word16 innerExtLen; + + /* get the unpadded length */ + idx = ech_tamper_seek_extension(innerCh, &innerExtLen); + idx += innerExtLen; + + /* no padding, but the test would fail if the message is not incorrect... + * so fail the callback */ + if (idx == innerChLen) { + return BAD_FUNC_ARG; + } + else { + innerCh[idx] = '\x01'; + return 0; + } +} + +static int ech_tamper_type(byte* innerCh, word32 innerChLen) +{ + int ret; + word16 idx; + + (void)innerChLen; + + ret = ech_tamper_find_extension(innerCh, &idx, TLSXT_ECH); + if (ret == 0) { + /* change type to outer */ + innerCh[idx + 4] = ECH_TYPE_OUTER; + return 0; + } + else { + return ret; + } +} + +static int ech_tamper_key_share(byte* innerCh, word32 innerChLen) +{ + int ret; + word16 idx; + word16 len; + + (void)innerChLen; + + ret = ech_tamper_find_extension(innerCh, &idx, TLSXT_KEY_SHARE); + if (ret == 0) { + ato16(innerCh + idx + 8, &len); + if (len == 0) { + return BAD_FUNC_ARG; + } + else { + /* tamper with public key data */ + innerCh[idx + 10] ^= 0xFF; + return 0; + } + } + else { + return ret; + } +} + +static int ech_tamper_ciphersuite(byte* innerCh, word32 innerChLen) +{ + word16 idx; + byte sessionIdLen; + word16 cipherSuitesLen; + + (void)innerChLen; + + idx = OPAQUE16_LEN + RAN_LEN; + + sessionIdLen = innerCh[idx++]; + idx += sessionIdLen; + + ato16(innerCh + idx, &cipherSuitesLen); + idx += OPAQUE16_LEN; + + if (cipherSuitesLen < 2) { + return BAD_FUNC_ARG; + } + else { + /* change all ciphersuites to unknown value */ + while (cipherSuitesLen > 0) { + innerCh[idx] = '\xFA'; + innerCh[idx + 1] = '\xFA'; + idx += OPAQUE16_LEN; + cipherSuitesLen -= OPAQUE16_LEN; + } + return 0; + } +} + +static int test_wolfSSL_Tls13_ECH_tamper_ex(struct test_ssl_memio_ctx* test_ctx) +{ + EXPECT_DECLS; + + test_ssl_memio_cleanup(test_ctx); + XMEMSET(test_ctx, 0, sizeof(struct test_ssl_memio_ctx)); + + test_ctx->s_cb.method = wolfTLSv1_3_server_method; + test_ctx->c_cb.method = wolfTLSv1_3_client_method; + + test_ctx->s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx->s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx->c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(test_ctx), TEST_SUCCESS); + return EXPECT_RESULT(); +} + +static int test_wolfSSL_Tls13_ECH_tamper_client(void) +{ + EXPECT_DECLS; + int err; + struct test_ssl_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + + /* try to downgrade to TLS 1.2 in the inner hello */ + test_ctx.s_cb.method = wolfSSLv23_server_method; + test_ctx.c_cb.method = wolfSSLv23_client_method; + + test_ctx.s_cb.ctx_ready = test_ech_server_ctx_ready; + test_ctx.s_cb.ssl_ready = test_ech_server_ssl_ready; + test_ctx.c_cb.ssl_ready = test_ech_client_ssl_ready; + + ExpectIntEQ(test_ssl_memio_setup(&test_ctx), TEST_SUCCESS); + + /* change supported_versions extension type to 0xFAFA: this will encourage a + * downgrade to TLS 1.2 */ + test_ctx.c_ssl->echInnerHelloCb = ech_tamper_downgrade; + + /* the server MUST reject an inner ClientHello that tries to negotiate + * TLS 1.2 or below */ + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(INVALID_PARAMETER)); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(FATAL_ERROR)); + + /* non-zero padding byte */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_tamper_ex(&test_ctx), TEST_SUCCESS); + test_ctx.c_ssl->echInnerHelloCb = ech_tamper_padding; + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + if (EXPECT_SUCCESS()) { + /* padding may have a length of zero which is not an error but the + * callback will treat it as such (thus the BAD_FUNC_ARG) */ + err = wolfSSL_get_error(test_ctx.s_ssl, 0); + ExpectTrue(err == WC_NO_ERR_TRACE(INVALID_PARAMETER) || + err == WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + } + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(FATAL_ERROR)); + + /* bad ECH type */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_tamper_ex(&test_ctx), TEST_SUCCESS); + test_ctx.c_ssl->echInnerHelloCb = ech_tamper_type; + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + ExpectIntEQ(wolfSSL_get_error(test_ctx.s_ssl, 0), + WC_NO_ERR_TRACE(INVALID_PARAMETER)); + ExpectIntEQ(wolfSSL_get_error(test_ctx.c_ssl, 0), + WC_NO_ERR_TRACE(FATAL_ERROR)); + + /* corrupted key share */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_tamper_ex(&test_ctx), TEST_SUCCESS); + test_ctx.c_ssl->echInnerHelloCb = ech_tamper_key_share; + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + + /* bad ciphersuite */ + ExpectIntEQ(test_wolfSSL_Tls13_ECH_tamper_ex(&test_ctx), TEST_SUCCESS); + test_ctx.c_ssl->echInnerHelloCb = ech_tamper_ciphersuite; + ExpectIntNE(test_ssl_memio_do_handshake(&test_ctx, 10, NULL), TEST_SUCCESS); + + test_ssl_memio_cleanup(&test_ctx); + return EXPECT_RESULT(); +} +#endif /* WOLFSSL_TLS13 && HAVE_ECH && WOLFSSL_TEST_ECH && + * HAVE_SSL_MEMIO_TESTS_DEPENDENCIES && !WOLFSSL_NO_TLS12 */ + #endif /* HAVE_ECH && WOLFSSL_TLS13 */ #if defined(HAVE_IO_TESTS_DEPENDENCIES) && \ @@ -36315,16 +37105,27 @@ TEST_CASE testCases[] = { /* Uses Assert in handshake callback. */ TEST_DECL(test_wolfSSL_Tls13_ECH), TEST_DECL(test_wolfSSL_Tls13_ECH_HRR), - TEST_DECL(test_wolfSSL_SubTls13_ECH), #endif #if defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) TEST_DECL(test_wolfSSL_Tls13_ECH_all_algos), TEST_DECL(test_wolfSSL_Tls13_ECH_no_private_name), TEST_DECL(test_wolfSSL_Tls13_ECH_bad_configs), + TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs), + TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs_bad), + TEST_DECL(test_wolfSSL_Tls13_ECH_retry_configs_auth_fail), TEST_DECL(test_wolfSSL_Tls13_ECH_new_config), TEST_DECL(test_wolfSSL_Tls13_ECH_GREASE), TEST_DECL(test_wolfSSL_Tls13_ECH_disable_conn), TEST_DECL(test_wolfSSL_Tls13_ECH_long_SNI), + TEST_DECL(test_wolfSSL_Tls13_ECH_HRR_rejection), + TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_no_ech), + TEST_DECL(test_wolfSSL_Tls13_ECH_ch2_decrypt_error), + TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_cert_valid), + TEST_DECL(test_wolfSSL_Tls13_ECH_rejected_empty_client_cert), +#endif +#if defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) && defined(WOLFSSL_TEST_ECH) && \ + !defined(WOLFSSL_NO_TLS12) + TEST_DECL(test_wolfSSL_Tls13_ECH_tamper_client), #endif TEST_DECL(test_wolfSSL_Tls13_ECH_enable_disable), #endif /* WOLFSSL_TLS13 && HAVE_ECH */ diff --git a/wolfssl/error-ssl.h b/wolfssl/error-ssl.h index 832ae9f440..d6d40f7748 100644 --- a/wolfssl/error-ssl.h +++ b/wolfssl/error-ssl.h @@ -240,7 +240,9 @@ enum wolfSSL_ErrorCodes { SESSION_TICKET_NONCE_OVERFLOW = -517, /* Session ticket nonce overflow */ - WOLFSSL_LAST_E = -517 + ECH_REQUIRED_E = -518, /* ECH offered but rejected by server */ + + WOLFSSL_LAST_E = -518 /* codes -1000 to -1999 are reserved for wolfCrypt. */ }; diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 7ef18d6c7f..6639377b0a 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3169,6 +3169,9 @@ WOLFSSL_LOCAL int GetEchConfigsEx(WOLFSSL_EchConfig* configs, byte* output, word32* outputLen); WOLFSSL_LOCAL void FreeEchConfigs(WOLFSSL_EchConfig* configs, void* heap); + +WOLFSSL_LOCAL int SetRetryConfigs(WOLFSSL* ssl, const byte* echConfigs, + word32 echConfigsLen); #endif struct TLSX { @@ -5181,7 +5184,9 @@ struct Options { #endif /* WOLFSSL_DTLS_CID */ #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) word16 echAccepted:1; - byte disableECH:1; /* Did the user disable ech */ + word16 disableECH:1; /* Did the user disable ech */ + word16 echProcessingInner:1; /* Processing the inner hello */ + word16 echRetryConfigsAccepted:1; #endif #ifdef WOLFSSL_SEND_HRR_COOKIE word16 cookieGood:1; @@ -6500,6 +6505,13 @@ struct WOLFSSL { #endif /* WOLFSSL_QUIC */ #if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) WOLFSSL_EchConfig* echConfigs; + WOLFSSL_EchConfig* echRetryConfigs; +#endif +#if defined(WOLFSSL_TLS13) && defined(HAVE_ECH) && defined(WOLFSSL_TEST_ECH) + /* Test-only hook: called on the client before ECH encryption, after the + * inner ClientHello body is fully constructed. The callback may modify + * innerCh in-place (length stays the same). */ + int (*echInnerHelloCb)(byte* innerCh, word32 innerChLen); #endif #if defined(WOLFSSL_SNIFFER) && defined(WOLFSSL_SNIFFER_KEYLOGFILE) diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 236515157b..27d0de0881 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -948,7 +948,7 @@ typedef struct WOLFSSL_ALERT_HISTORY { /* Valid Alert types from page 16/17 - * Add alert string to the function AlertTypeToString in src/ssl.c + * Add alert string to the function AlertTypeToString in src/internal.c */ enum AlertDescription { invalid_alert = -1, @@ -986,7 +986,8 @@ enum AlertDescription { bad_certificate_status_response = 113, /**< RFC 6066, section 8 */ unknown_psk_identity = 115, /**< RFC 4279, section 2 */ certificate_required = 116, /**< RFC 8446, section 8.2 */ - no_application_protocol = 120 + no_application_protocol = 120, + ech_required = 121 /**< RFC 9849, section 5 */ }; #ifdef WOLFSSL_MYSQL_COMPATIBLE @@ -1239,6 +1240,9 @@ WOLFSSL_API int wolfSSL_SetEchConfigs(WOLFSSL* ssl, const byte* echConfigs, WOLFSSL_API int wolfSSL_GetEchConfigs(WOLFSSL* ssl, byte* echConfigs, word32* echConfigsLen); +WOLFSSL_API int wolfSSL_GetEchRetryConfigs(WOLFSSL* ssl, byte* echConfigs, + word32* echConfigsLen); + WOLFSSL_API void wolfSSL_SetEchEnable(WOLFSSL* ssl, byte enable); #endif /* WOLFSSL_TLS13 && HAVE_ECH */