diff -Nru srt-1.5.4/debian/changelog srt-1.5.4/debian/changelog --- srt-1.5.4/debian/changelog 2024-11-13 16:27:08.000000000 +0100 +++ srt-1.5.4/debian/changelog 2026-08-15 14:34:27.000000000 +0200 @@ -1,3 +1,16 @@ +srt (1.5.4-1+deb13u1) trixie-security; urgency=medium + + * [4136a38] d/patches/CVE-2026-55869.patch: added from upstream. + Fix KMREQ/KMRSP Stack-Based Buffer Overflow (CVE-2026-55869, + GHSA-6xg9-784j-24rm), when used together with the patch for + CVE-2026-55868 / GHSA-4mc6-qmpp-g7gw. + * [76b1e2d] d/patches/CVE-2026-55868.patch: added from upstream. + Fix Encryption State Machine Downgrade (CVE-2026-55868, + GHSA-4mc6-qmpp-g7gw), and fix KMREQ/KMRSP Stack-Based Buffer Overflow + (CVE-2026-55869, GHSA-6xg9-784j-24rm). + + -- Florian Ernst Sat, 15 Aug 2026 14:34:27 +0200 + srt (1.5.4-1) unstable; urgency=medium * [9ec29e2] New upstream version 1.5.4 diff -Nru srt-1.5.4/debian/patches/CVE-2026-55868.patch srt-1.5.4/debian/patches/CVE-2026-55868.patch --- srt-1.5.4/debian/patches/CVE-2026-55868.patch 1970-01-01 01:00:00.000000000 +0100 +++ srt-1.5.4/debian/patches/CVE-2026-55868.patch 2026-08-15 14:22:53.000000000 +0200 @@ -0,0 +1,1746 @@ +Origin: upstream, https://github.com/Haivision/srt/pull/3345 +Applied-Upstream: v1.5.6, https://github.com/Haivision/srt/releases/tag/v1.5.6 +From d17cc7b4dbe4658eb3ac71c7eec3c71780553250 Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Fri, 5 Jun 2026 14:09:18 +0200 +Subject: [PATCH 1/8] Mimimum patch to fix spoofed MKREQ failure + +--- + haicrypt/haicrypt.h | 3 + + haicrypt/hcrypt_ctx_rx.c | 30 +- + haicrypt/hcrypt_rx.c | 30 ++ + srtcore/core.cpp | 11 +- + srtcore/core.h | 3 +- + srtcore/crypto.cpp | 379 +++++++++++---------- + srtcore/crypto.h | 2 + + test/test_bonding.cpp | 4 +- + test/test_crypto.cpp | 695 ++++++++++++++++++++++++++++++++++----- + 9 files changed, 879 insertions(+), 278 deletions(-) + +diff --git a/haicrypt/haicrypt.h b/haicrypt/haicrypt.h +index da0ad3493..573efbf3b 100644 +--- a/haicrypt/haicrypt.h ++++ b/haicrypt/haicrypt.h +@@ -106,6 +106,9 @@ int HaiCrypt_Tx_Process(HaiCrypt_Handle hhc, unsigned char *in, size_t in_len, + int HaiCrypt_Rx_Process(HaiCrypt_Handle hhc, unsigned char *in, size_t in_len, + void *out_p[], size_t out_len_p[], int maxout); + ++// Length not needed, as long as it's pre-verified that it's at least SALT length. ++int HaiCrypt_GetKeyIndex(HaiCrypt_Handle hhc, unsigned char *in); ++ + int HaiCrypt_Tx_GetKeyFlags(HaiCrypt_Handle hhc); + int HaiCrypt_Tx_ManageKeys(HaiCrypt_Handle hhc, void *out_p[], size_t out_len_p[], int maxout); + int HaiCrypt_Tx_Data(HaiCrypt_Handle hhc, unsigned char *pfx, unsigned char *data, size_t data_len); +diff --git a/haicrypt/hcrypt_ctx_rx.c b/haicrypt/hcrypt_ctx_rx.c +index 2b67490d3..9e2dafbfa 100644 +--- a/haicrypt/hcrypt_ctx_rx.c ++++ b/haicrypt/hcrypt_ctx_rx.c +@@ -152,24 +152,25 @@ int hcryptCtx_Rx_ParseKM(hcrypt_Session *crypto, unsigned char *km_msg, size_t m + return(-1); + } + ++ hcrypt_Ctx new_ctx = *ctx; + /* Check Salt and get if new */ +- if ((salt_len != ctx->salt_len) +- || (0 != memcmp(ctx->salt, &km_msg[HCRYPT_MSG_KM_OFS_SALT], salt_len))) { ++ if ((salt_len != new_ctx.salt_len) ++ || (0 != memcmp(new_ctx.salt, &km_msg[HCRYPT_MSG_KM_OFS_SALT], salt_len))) { + /* Salt changed (or 1st KMmsg received) */ +- memcpy(ctx->salt, &km_msg[HCRYPT_MSG_KM_OFS_SALT], salt_len); +- ctx->salt_len = salt_len; ++ memcpy(new_ctx.salt, &km_msg[HCRYPT_MSG_KM_OFS_SALT], salt_len); ++ new_ctx.salt_len = salt_len; + do_pbkdf = 1; /* Impact on password derived kek */ + } + + /* Check SEK length and get if new */ +- if (sek_len != ctx->sek_len) { ++ if (sek_len != new_ctx.sek_len) { + /* Key length changed or 1st KMmsg received */ +- ctx->sek_len = sek_len; ++ new_ctx.sek_len = sek_len; + do_pbkdf = 1; /* Impact on password derived kek */ + } + + /* Check cipher mode */ +- if (ctx->mode != km_msg[HCRYPT_MSG_KM_OFS_CIPHER]) ++ if (new_ctx.mode != km_msg[HCRYPT_MSG_KM_OFS_CIPHER]) + { + HCRYPT_LOG(LOG_WARNING, "%s", "cipher mode mismatch\n"); + return(-3); +@@ -179,21 +180,24 @@ int hcryptCtx_Rx_ParseKM(hcrypt_Session *crypto, unsigned char *km_msg, size_t m + * Regenerate KEK if it is password derived + * and Salt or SEK length changed + */ +- if (ctx->cfg.pwd_len && do_pbkdf) { +- if (hcryptCtx_GenSecret(crypto, ctx)) { ++ if (new_ctx.cfg.pwd_len && do_pbkdf) { ++ if (hcryptCtx_GenSecret(crypto, &new_ctx)) { + return(-1); + } +- ctx->status = HCRYPT_CTX_S_SARDY; ++ new_ctx.status = HCRYPT_CTX_S_SARDY; + kek_len = sek_len; /* KEK changed */ + } + + /* Unwrap SEK(s) and set in context */ +- if (0 > crypto->cryspr->km_unwrap(crypto->cryspr_cb, seks, +- &km_msg[HCRYPT_MSG_KM_OFS_SALT + salt_len], +- (unsigned int)((sek_cnt * sek_len) + HAICRYPT_WRAPKEY_SIGN_SZ))) { ++ unsigned int msglen = (sek_cnt * sek_len) + HAICRYPT_WRAPKEY_SIGN_SZ; ++ int wrc = crypto->cryspr->km_unwrap(crypto->cryspr_cb, seks, ++ &km_msg[HCRYPT_MSG_KM_OFS_SALT + salt_len], msglen); ++ ++ if (wrc < 0) { + HCRYPT_LOG(LOG_WARNING, "%s", "unwrap key failed\n"); + return(-2); //Report unmatched shared secret + } ++ *ctx = new_ctx; + /* + * First SEK in KMmsg is eSEK if both SEK present + */ +diff --git a/haicrypt/hcrypt_rx.c b/haicrypt/hcrypt_rx.c +index 2f7e84739..4cb3f84d5 100644 +--- a/haicrypt/hcrypt_rx.c ++++ b/haicrypt/hcrypt_rx.c +@@ -61,6 +61,29 @@ int HaiCrypt_Rx_Data(HaiCrypt_Handle hhc, + return(nb); + } + ++int HaiCrypt_GetKeyIndex(HaiCrypt_Handle hhc, unsigned char* in_msg) ++{ ++ hcrypt_Session *crypto = (hcrypt_Session *)hhc; ++ int msg_type; ++ ++ if ((NULL == crypto) ++ || (NULL == in_msg)) { ++ ++ HCRYPT_LOG(LOG_ERR, "%s", "invalid parameters\n"); ++ return(-1); ++ } ++ ++ /* Validate HaiCrypt message */ ++ if (0 > (msg_type = hcryptMsg_SRT_ParseMsg(crypto->msg_info, in_msg))) { ++ return(-1); ++ } ++ ++ if (msg_type != HCRYPT_MSG_PT_KM) ++ return -1; ++ ++ return hcryptMsg_GetKeyIndex(crypto->msg_info, in_msg); ++} ++ + int HaiCrypt_Rx_Process(HaiCrypt_Handle hhc, + unsigned char *in_msg, size_t in_len, + void *out_p[], size_t out_len_p[], int maxout) +@@ -99,6 +122,13 @@ int HaiCrypt_Rx_Process(HaiCrypt_Handle hhc, + HCRYPT_LOG(LOG_ERR, "%s", "cryspr had no decryptor\n"); + nbout = -1; + } else if (ctx->status >= HCRYPT_CTX_S_KEYED) { ++ if ((int)(in_len) <= crypto->msg_info->pfx_len) ++ { ++ // XXX NOTE: SRT doesn't use this type of messages, so it shouldn't ++ // be a danger, but then you can still find something like that on the wire. ++ HCRYPT_LOG(LOG_ERR, "%s%d", "Invalid enc data packet: ", in_len); ++ return -1; ++ } + hcrypt_DataDesc indata; + indata.pfx = in_msg; + indata.payload = &in_msg[crypto->msg_info->pfx_len]; +diff --git a/srtcore/core.cpp b/srtcore/core.cpp +index e4577a27d..5f12c2527 100644 +--- a/srtcore/core.cpp ++++ b/srtcore/core.cpp +@@ -10163,6 +10163,7 @@ int srt::CUDT::handleSocketPacketReception(const vector& incoming, bool& + } + } + ++ bool decrypt_successful = false; + const int buffer_add_result = m_pRcvBuffer->insert(u); + if (buffer_add_result < 0) + { +@@ -10223,8 +10224,12 @@ int srt::CUDT::handleSocketPacketReception(const vector& incoming, bool& + } + #endif + } ++ else ++ { ++ decrypt_successful = true; ++ } + } +- else if (m_pCryptoControl && m_pCryptoControl->m_RcvKmState == SRT_KM_S_SECURED) ++ else if (m_pCryptoControl && m_pCryptoControl->m_RcvKmState != SRT_KM_S_UNSECURED) + { + // Unencrypted packets are not allowed. + const int iDropCnt = m_pRcvBuffer->dropMessage(u->m_Packet.getSeqNo(), u->m_Packet.getSeqNo(), SRT_MSGNO_NONE, CRcvBuffer::DROP_EXISTING); +@@ -10303,6 +10308,10 @@ int srt::CUDT::handleSocketPacketReception(const vector& incoming, bool& + if (CSeqNo::seqcmp(rpkt.seqno(), m_iRcvCurrSeqNo) > 0) + { + m_iRcvCurrSeqNo = rpkt.seqno(); // Latest possible received ++ if (decrypt_successful && m_pCryptoControl) ++ { ++ m_pCryptoControl->m_CurrentKey = rpkt.getMsgCryptoFlags(); ++ } + } + else + { +diff --git a/srtcore/core.h b/srtcore/core.h +index a4a9acaee..31cedbd4f 100644 +--- a/srtcore/core.h ++++ b/srtcore/core.h +@@ -149,10 +149,9 @@ enum SeqPairItems + }; + + +-// Extended SRT Congestion control class - only an incomplete definition required +-class CCryptoControl; + + namespace srt { ++class CCryptoControl; + class CUDTUnited; + class CUDTSocket; + #if ENABLE_BONDING +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index 20c8ba47e..b8d511da3 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -135,10 +135,10 @@ void srt::CCryptoControl::createFakeSndContext() + } + } + ++#ifdef SRT_ENABLE_ENCRYPTION ++ + int srt::CCryptoControl::processSrtMsg_KMREQ( +- const uint32_t* srtdata SRT_ATR_UNUSED, +- size_t bytelen SRT_ATR_UNUSED, +- int hsv SRT_ATR_UNUSED, unsigned srtv SRT_ATR_UNUSED, ++ const uint32_t* srtdata, size_t bytelen, int hsv, unsigned srtv, + uint32_t pw_srtdata_out[], size_t& w_srtlen) + { + //Receiver +@@ -146,7 +146,8 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + * But HaiCrypt expect network order message + * Re-swap to cancel it. + */ +-#ifdef SRT_ENABLE_ENCRYPTION ++ ++ // Default successful settinsga: original length and contents + w_srtlen = bytelen/sizeof(srtdata[SRT_KMR_KMSTATE]); + HtoNLA((pw_srtdata_out), srtdata, w_srtlen); + unsigned char* kmdata = reinterpret_cast(pw_srtdata_out); +@@ -155,18 +156,17 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + // what has called this function. The HSv5 handshake only enforces bidirectional + // connection. + ++ // They actually mean the same thing in HSv5, but removal of bidirectional can be ++ // only done when the compat with <1.3.0 is allowed to be broken. + const bool bidirectional = hsv > CUDT::HS_VERSION_UDT4; ++ const bool kmx_update = m_hRcvCrypto; ++ SRT_KM_STATE failure_state = m_KmSecret.len == 0 ? SRT_KM_S_NOSECRET : SRT_KM_S_BADSECRET; + +- // Local macro to return rejection appropriately. +- // CHANGED. The first version made HSv5 reject the connection. +- // This isn't well handled by applications, so the connection is +- // still established, but unable to handle any transport. +-//#define KMREQ_RESULT_REJECTION() if (bidirectional) { return SRT_CMD_NONE; } else { w_srtlen = 1; goto HSv4_ErrorReport; } +-#define KMREQ_RESULT_REJECTION() { w_srtlen = 1; goto HSv4_ErrorReport; } +- +- int rc = HAICRYPT_OK; // needed before 'goto' run from KMREQ_RESULT_REJECTION macro +- bool wasb4 SRT_ATR_UNUSED = false; +- size_t sek_len = 0; ++ if (!kmx_update) // Only in initial/handshake ++ { ++ // If this is NOT changed anywhere later, failure_state value will be used ++ m_RcvKmState = SRT_KM_S_SECURING; ++ } + + const bool bUseGCM = + (m_iCryptoMode == CSrtConfig::CIPHER_MODE_AUTO && kmdata[HCRYPT_MSG_KM_OFS_CIPHER] == HCRYPT_CIPHER_AES_GCM) || +@@ -174,198 +174,204 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + + m_bUseGcm153 = srtv <= SrtVersion(1, 5, 3); + +- // What we have to do: +- // If encryption is on (we know that by having m_KmSecret nonempty), create +- // the crypto context (if bidirectional, create for both sending and receiving). +- // Both crypto contexts should be set with the same length of the key. +- // The problem with interpretinting this should be reported as SRT_CMD_NONE, +- // should be appropriately handled by the caller, as it expects that this +- // function normally return SRT_CMD_KMRSP. +- if ( bytelen <= HCRYPT_MSG_KM_OFS_SALT ) //Sanity on message ++ // TRY-BLOCK, with THROW done by "goto Error". + { +- LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: size of the KM (" << bytelen << ") is too small, must be >" << HCRYPT_MSG_KM_OFS_SALT); +- m_RcvKmState = SRT_KM_S_BADSECRET; +- KMREQ_RESULT_REJECTION(); +- } ++ // INITIAL ACTIONS (first time KMREQ received): ++ // If encryption is on (we know that by having m_KmSecret nonempty), create ++ // the crypto context (if bidirectional, create for both sending and receiving). ++ // Both crypto contexts should be set with the same length of the key. ++ // Report SRT_CMD_NONE if no response is to be sent or SRT_CMD_KMRSP if the ++ // response is filled and ready (including erroneous). ++ if (bytelen <= HCRYPT_MSG_KM_OFS_SALT) //Sanity on message ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: size of the KM (" << bytelen << ") is too small, must be >" << HCRYPT_MSG_KM_OFS_SALT); ++ goto Error; ++ } + +- HLOGC(cnlog.Debug, log << "KMREQ: getting SEK and creating receiver crypto"); +- sek_len = hcryptMsg_KM_GetSekLen(kmdata); +- if ( sek_len == 0 ) +- { +- LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Received SEK is empty - REJECTING!"); +- m_RcvKmState = SRT_KM_S_BADSECRET; +- KMREQ_RESULT_REJECTION(); +- } ++ size_t sek_len = hcryptMsg_KM_GetSekLen(kmdata); ++ if (sek_len == 0) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Received SEK is empty - REJECTING!"); ++ goto Error; ++ } + +- // Write the key length +- m_iRcvKmKeyLen = sek_len; +- // Overwrite the key length anyway - it doesn't make sense to somehow +- // keep the original setting because it will only make KMX impossible. +-#if ENABLE_HEAVY_LOGGING +- if (m_iSndKmKeyLen != m_iRcvKmKeyLen) +- { +- LOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: Agent's PBKEYLEN=" << m_iSndKmKeyLen +- << " overwritten by Peer's PBKEYLEN=" << m_iRcvKmKeyLen); +- } +-#endif +- m_iSndKmKeyLen = m_iRcvKmKeyLen; ++ bool new_keylen = m_iSndKmKeyLen != sek_len; + +- // This is checked only now so that the SRTO_PBKEYLEN return always the correct value, +- // even if encryption is not possible because Agent didn't set a password, or supplied +- // a wrong password. +- if (m_KmSecret.len == 0) //We have a shared secret <==> encryption is on +- { +- LOGC(cnlog.Warn, log << "processSrtMsg_KMREQ: Agent does not declare encryption - won't decrypt incoming packets!"); +- m_RcvKmState = SRT_KM_S_NOSECRET; +- KMREQ_RESULT_REJECTION(); +- } +- wasb4 = m_hRcvCrypto; ++ if (kmx_update && new_keylen) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: KMX refresh came in with a DIFFERENT KEY LEN: " << sek_len); ++ goto Error; ++ } + +- if (!createCryptoCtx((m_hRcvCrypto), m_iRcvKmKeyLen, HAICRYPT_CRYPTO_DIR_RX, bUseGCM)) +- { +- LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Can't create RCV CRYPTO CTX - must reject..."); +- m_RcvKmState = SRT_KM_S_NOSECRET; +- KMREQ_RESULT_REJECTION(); +- } ++ HLOGC(cnlog.Debug, log << "KMREQ: getting SEK and creating receiver crypto sek_len=" << sek_len ++ << " local=" << m_iSndKmKeyLen ++ << (new_keylen ? " - OVERRIDE with received SEK len" : "")); + +- // Deduce resulting mode. +- m_iCryptoMode = bUseGCM ? CSrtConfig::CIPHER_MODE_AES_GCM : CSrtConfig::CIPHER_MODE_AES_CTR; ++ // Write the key length. ++ // Overwrite the key length anyway - it doesn't make sense to somehow ++ // keep the original setting because it will only make KMX impossible. ++ m_iSndKmKeyLen = m_iRcvKmKeyLen = sek_len; + +- if (!wasb4) +- { +- HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: created RX ENC with KeyLen=" << m_iRcvKmKeyLen); +- } +- // We have both sides set with password, so both are pending for security +- m_RcvKmState = SRT_KM_S_SECURING; +- // m_SndKmState is set to SECURING or UNSECURED in init(), +- // or it might have been set to SECURED, NOSECRET or BADSECRET in the previous +- // handshake iteration (handshakes may be sent multiple times for the same connection). +- +- rc = HaiCrypt_Rx_Process(m_hRcvCrypto, kmdata, bytelen, NULL, NULL, 0); +- switch(rc >= 0 ? HAICRYPT_OK : rc) +- { +- case HAICRYPT_OK: +- m_RcvKmState = SRT_KM_S_SECURED; +- HLOGC(cnlog.Debug, log << "KMREQ/rcv: (snd) Rx process successful - SECURED."); +- //Send back the whole message to confirm +- break; +- case HAICRYPT_ERROR_WRONG_SECRET: //Unmatched shared secret to decrypt wrapped key +- m_RcvKmState = m_SndKmState = SRT_KM_S_BADSECRET; +- //Send status KMRSP message to tel error +- w_srtlen = 1; +- LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADSECRET"); +- break; +- case HAICRYPT_ERROR_CIPHER: ++ // This is checked only now so that the SRTO_PBKEYLEN return always the correct value, ++ // even if encryption is not possible because Agent didn't set a password, or supplied ++ // a wrong password. ++ if (!hasPassphrase()) //We have a shared secret <==> encryption is on ++ { ++ LOGC(cnlog.Warn, log << "processSrtMsg_KMREQ: Agent does not declare encryption - won't decrypt incoming packets!"); ++ goto Error; ++ } ++ ++ if (!createCryptoCtx((m_hRcvCrypto), m_iRcvKmKeyLen, HAICRYPT_CRYPTO_DIR_RX, bUseGCM)) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Can't create RCV CRYPTO CTX - must reject..."); ++ failure_state = SRT_KM_S_NOSECRET; ++ goto Error; ++ } ++ ++ if (!kmx_update) ++ { ++ // Deduce resulting mode. ++ m_iCryptoMode = bUseGCM ? CSrtConfig::CIPHER_MODE_AES_GCM : CSrtConfig::CIPHER_MODE_AES_CTR; ++ HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: created RX ENC with KeyLen=" << m_iRcvKmKeyLen); ++ } ++ else if (m_CurrentKey != EK_NOENC) // Received at least 1 encrypted packet ++ { ++ // HaiCrypt_GetKeyIndex returns 0 or 1 as key index or -1 as error; so ++ // adding 1 results in 1, 2 and 0 respectively, which correspond to ++ // EK_EVEN, EK_ODD and EK_NOENC respectively, the latter being an error. ++ int keyindex = HaiCrypt_GetKeyIndex(m_hRcvCrypto, kmdata) + 1; ++ ++ if (keyindex == EK_NOENC || keyindex != m_CurrentKey) ++ goto Error; // Will result in BADSECRET ++ } ++ ++ // We have both sides set with password, so both are pending for security ++ int rc = HaiCrypt_Rx_Process(m_hRcvCrypto, kmdata, bytelen, NULL, NULL, 0); ++ LOGC(cnlog.Note, log << FormatKmMessage("processSrtMsg_KMREQ", SRT_CMD_KMREQ, bytelen) << " result: " << rc); ++ ++ // Since now, when CCryptoControl::decrypt() encounters an error, it will print it, ONCE, ++ // until the next KMREQ is received as a key regeneration. ++ m_bErrorReported = false; ++ ++ if (rc >= HAICRYPT_OK) ++ { ++ m_RcvKmState = SRT_KM_S_SECURED; ++ HLOGC(cnlog.Debug, log << "KMREQ/rcv: (snd) Rx process successful - SECURED."); ++ } ++ else ++ { ++ SRT_KM_STATE failstate; ++ switch(rc) ++ { ++ case HAICRYPT_ERROR_WRONG_SECRET: //Unmatched shared secret to decrypt wrapped key ++ failstate = SRT_KM_S_BADSECRET; ++ //Send status KMRSP message to tel error ++ LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADSECRET"); ++ break; ++ case HAICRYPT_ERROR_CIPHER: + #ifdef ENABLE_AEAD_API_PREVIEW +- m_RcvKmState = m_SndKmState = SRT_KM_S_BADCRYPTOMODE; ++ failstate = SRT_KM_S_BADCRYPTOMODE; + #else +- m_RcvKmState = m_SndKmState = SRT_KM_S_BADSECRET; // Use "bad secret" as a fallback. ++ failstate = SRT_KM_S_BADSECRET; // Use "bad secret" as a fallback. + #endif +- w_srtlen = 1; +- LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADCRYPTOMODE"); +- break; +- case HAICRYPT_ERROR: //Other errors +- default: +- m_RcvKmState = m_SndKmState = SRT_KM_S_NOSECRET; +- w_srtlen = 1; +- LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure (IPE) - NOSECRET"); +- break; +- } +- +- LOGP(cnlog.Note, FormatKmMessage("processSrtMsg_KMREQ", SRT_CMD_KMREQ, bytelen)); +- +- // Since now, when CCryptoControl::decrypt() encounters an error, it will print it, ONCE, +- // until the next KMREQ is received as a key regeneration. +- m_bErrorReported = false; ++ LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADCRYPTOMODE"); ++ break; ++ case HAICRYPT_ERROR: //Other errors ++ default: ++ failstate = SRT_KM_S_NOSECRET; ++ LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure (IPE) - NOSECRET"); ++ break; ++ } + +- if (w_srtlen == 1) +- goto HSv4_ErrorReport; ++ if (!kmx_update) // DO NOT change any state if it was KMX update. ++ { ++ m_RcvKmState = m_SndKmState = failstate; ++ } ++ goto Error; ++ } + +- // Configure the sender context also, if it succeeded to configure the +- // receiver context and we are using bidirectional mode. +- if (bidirectional) +- { +- // Note: 'bidirectional' means that we want a bidirectional key update, +- // which happens only and exclusively with HSv5 handshake - not when the +- // usual key update through UMSG_EXT+SRT_CMD_KMREQ was done (which is used +- // in HSv4 versions also to initialize the first key, unlike HSv5). +- if (m_RcvKmState == SRT_KM_S_SECURED) ++ // Configure the sender context also, if it succeeded to configure the ++ // receiver context and we are using bidirectional mode. ++ if (bidirectional) + { +- if (m_SndKmState == SRT_KM_S_SECURING && !m_hSndCrypto) ++ // Note: 'bidirectional' means that we want a bidirectional key update, ++ // which happens only and exclusively with HSv5 handshake - not when the ++ // usual key update through UMSG_EXT+SRT_CMD_KMREQ was done (which is used ++ // in HSv4 versions also to initialize the first key, unlike HSv5). ++ if (m_RcvKmState == SRT_KM_S_SECURED) + { +- m_iSndKmKeyLen = m_iRcvKmKeyLen; +- if (HaiCrypt_Clone(m_hRcvCrypto, HAICRYPT_CRYPTO_DIR_TX, &m_hSndCrypto) != HAICRYPT_OK) ++ if (m_SndKmState == SRT_KM_S_SECURING && !m_hSndCrypto) + { +- LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Can't create SND CRYPTO CTX - WILL NOT SEND-ENCRYPT correctly!"); +- if (hasPassphrase()) +- m_SndKmState = SRT_KM_S_BADSECRET; ++ m_iSndKmKeyLen = m_iRcvKmKeyLen; ++ if (HaiCrypt_Clone(m_hRcvCrypto, HAICRYPT_CRYPTO_DIR_TX, &m_hSndCrypto) != HAICRYPT_OK) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: Can't create SND CRYPTO CTX - WILL NOT SEND-ENCRYPT correctly!"); ++ m_SndKmState = failure_state; ++ } + else +- m_SndKmState = SRT_KM_S_NOSECRET; ++ { ++ m_SndKmState = SRT_KM_S_SECURED; ++ } ++ ++ LOGC(cnlog.Note, log << FormatKmMessage("processSrtMsg_KMREQ", SRT_CMD_KMREQ, bytelen) ++ << " SndKeyLen=" << m_iSndKmKeyLen ++ << " TX CRYPTO CTX CLONED FROM RX" ++ ); ++ ++ // Write the KM message into the field from which it will be next sent. ++ memcpy((m_SndKmMsg[0].Msg), kmdata, bytelen); ++ m_SndKmMsg[0].MsgLen = bytelen; ++ m_SndKmMsg[0].iPeerRetry = 0; // Don't start sending them upon connection :) + } + else + { +- m_SndKmState = SRT_KM_S_SECURED; ++ HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: NOT cloning RX to TX crypto: already in " ++ << KmStateStr(m_SndKmState) << " state"); + } +- +- LOGC(cnlog.Note, log << FormatKmMessage("processSrtMsg_KMREQ", SRT_CMD_KMREQ, bytelen) +- << " SndKeyLen=" << m_iSndKmKeyLen +- << " TX CRYPTO CTX CLONED FROM RX" +- ); +- +- // Write the KM message into the field from which it will be next sent. +- memcpy((m_SndKmMsg[0].Msg), kmdata, bytelen); +- m_SndKmMsg[0].MsgLen = bytelen; +- m_SndKmMsg[0].iPeerRetry = 0; // Don't start sending them upon connection :) + } + else + { +- HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: NOT cloning RX to TX crypto: already in " +- << KmStateStr(m_SndKmState) << " state"); ++ HLOGP(cnlog.Debug, "processSrtMsg_KMREQ: NOT SECURED - not replaying failed security association to TX CRYPTO CTX"); + } + } + else + { +- HLOGP(cnlog.Debug, "processSrtMsg_KMREQ: NOT SECURED - not replaying failed security association to TX CRYPTO CTX"); ++ HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: NOT REPLAYING the key update to TX CRYPTO CTX."); + } +- } +- else +- { +- HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: NOT REPLAYING the key update to TX CRYPTO CTX."); +- } +- +-#ifdef SRT_ENABLE_ENCRYPTION +- if (m_hRcvCrypto != NULL) +- HaiCrypt_UpdateGcm153(m_hRcvCrypto, m_bUseGcm153); +- if (m_hSndCrypto != NULL) +- HaiCrypt_UpdateGcm153(m_hSndCrypto, m_bUseGcm153); +-#endif +- +- return SRT_CMD_KMRSP; + +-HSv4_ErrorReport: ++ if (m_hRcvCrypto != NULL) ++ HaiCrypt_UpdateGcm153(m_hRcvCrypto, m_bUseGcm153); ++ if (m_hSndCrypto != NULL) ++ HaiCrypt_UpdateGcm153(m_hSndCrypto, m_bUseGcm153); + +- if (bidirectional && hasPassphrase()) +- { +- // If the Forward KMX process has failed, the reverse-KMX process was not done at all. +- // This will lead to incorrect object configuration and will fail to properly declare +- // the transmission state. +- // Create the "fake crypto" with the passphrsae you currently have. +- createFakeSndContext(); ++ // NOTE: The "loop" gets exit with return HERE. ++ return SRT_CMD_KMRSP; + } +-#undef KMREQ_RESULT_REJECTION +- +-#else +- // It's ok that this is reported as error because this happens in a scenario, +- // when non-encryption-enabled SRT application is contacted by encryption-enabled SRT +- // application which tries to make a security association. +- LOGC(cnlog.Warn, log << "processSrtMsg_KMREQ: Encryption not enabled at compile time - must reject..."); +- m_RcvKmState = SRT_KM_S_NOSECRET; +-#endif ++Error: // CATCH POINT + ++ // NOTE: This is set, but if we return NONE, this will not be ++ // taken into account anyhow. The state is returned this way only for ++ // unit tests so that they recognize the result of the call. + w_srtlen = 1; ++ pw_srtdata_out[SRT_KMR_KMSTATE] = failure_state; + +- pw_srtdata_out[SRT_KMR_KMSTATE] = m_RcvKmState; +- return SRT_CMD_KMRSP; ++ if (!kmx_update) ++ { ++ // Set the appropriate error, if it wasn't already set before ++ if (m_RcvKmState == SRT_KM_S_SECURING) ++ m_RcvKmState = failure_state; ++ if (bidirectional && hasPassphrase()) ++ { ++ // If the Forward KMX process has failed, the reverse-KMX process was not done at all. ++ // This will lead to incorrect object configuration and will fail to properly declare ++ // the transmission state. ++ // Create the "fake crypto" with the passphrsae you currently have. ++ createFakeSndContext(); ++ } ++ ++ return SRT_CMD_KMRSP; ++ } ++ return SRT_CMD_NONE; + } + + int srt::CCryptoControl::processSrtMsg_KMRSP(const uint32_t* srtdata, size_t len, unsigned srtv) +@@ -481,12 +487,10 @@ int srt::CCryptoControl::processSrtMsg_KMRSP(const uint32_t* srtdata, size_t len + << "; key[1]: len=" << m_SndKmMsg[1].MsgLen << " retry=" << m_SndKmMsg[1].iPeerRetry); + + m_bUseGcm153 = srtv <= SrtVersion(1, 5, 3); +-#ifdef SRT_ENABLE_ENCRYPTION + if (m_hRcvCrypto != NULL) + HaiCrypt_UpdateGcm153(m_hRcvCrypto, m_bUseGcm153); + if (m_hSndCrypto != NULL) + HaiCrypt_UpdateGcm153(m_hSndCrypto, m_bUseGcm153); +-#endif + } + + LOGP(cnlog.Note, FormatKmMessage("processSrtMsg_KMRSP", SRT_CMD_KMRSP, len)); +@@ -494,6 +498,30 @@ int srt::CCryptoControl::processSrtMsg_KMRSP(const uint32_t* srtdata, size_t len + return retstatus; + } + ++#else ++ ++int srt::CCryptoControl::processSrtMsg_KMREQ( ++ const uint32_t*, size_t, int, unsigned, // ignore input ++ uint32_t pw_srtdata_out[], size_t& w_srtlen) ++{ ++ // It's ok that this is reported as error because this happens in a scenario, ++ // when non-encryption-enabled SRT application is contacted by encryption-enabled SRT ++ // application which tries to make a security association. ++ LOGC(cnlog.Warn, log << "processSrtMsg_KMREQ: Encryption not enabled at compile time - must reject..."); ++ m_RcvKmState = SRT_KM_S_NOSECRET; ++ pw_srtdata_out[SRT_KMR_KMSTATE] = m_RcvKmState; ++ w_srtlen = 1; ++ ++ return SRT_CMD_KMRSP; ++} ++ ++int srt::CCryptoControl::processSrtMsg_KMRSP(const uint32_t*, size_t, unsigned) ++{ ++ LOGP(cnlog.Error, "processSrtMsg_KMRSP: Encryption not enabled at compile time; not expected to receive SRT_CMD_KMRSP"); ++ return SRT_CMD_NONE; ++} ++#endif ++ + void srt::CCryptoControl::sendKeysToPeer(CUDT* sock SRT_ATR_UNUSED, int iSRTT SRT_ATR_UNUSED) + { + sync::ScopedLock lck(m_mtxLock); +@@ -618,6 +646,7 @@ srt::CCryptoControl::CCryptoControl(SRTSOCKET id) + , m_iRcvKmKeyLen(0) + , m_SndKmState(SRT_KM_S_UNSECURED) + , m_RcvKmState(SRT_KM_S_UNSECURED) ++ , m_CurrentKey(EK_NOENC) + , m_KmRefreshRatePkt(0) + , m_KmPreAnnouncePkt(0) + , m_iCryptoMode(CSrtConfig::CIPHER_MODE_AUTO) +@@ -884,7 +909,7 @@ srt::EncryptionStatus srt::CCryptoControl::decrypt(CPacket& w_packet SRT_ATR_UNU + if (m_RcvKmState != SRT_KM_S_SECURED) + { + // If not "secured", it means that it won't be able to decrypt packets, +- // so there's no point to even try to send them to HaiCrypt_Rx_Data. ++ // so there's no point in even trying to send them to HaiCrypt_Rx_Data. + // Actually the current conditions concerning m_hRcvCrypto are such that this object + // is cretaed in case of SRT_KM_S_BADSECRET, so it will simply fail to decrypt, + // but with SRT_KM_S_NOSECRET m_hRcvCrypto is not even created (is NULL), which +diff --git a/srtcore/crypto.h b/srtcore/crypto.h +index 613ded8dd..a1bb42d99 100644 +--- a/srtcore/crypto.h ++++ b/srtcore/crypto.h +@@ -22,6 +22,7 @@ written by + // UDT + #include "udt.h" + #include "packet.h" ++#include "common.h" + #include "utilities.h" + #include "logging.h" + +@@ -61,6 +62,7 @@ class CCryptoControl + public: + SRT_KM_STATE m_SndKmState; //Sender Km State (imposed by agent) + SRT_KM_STATE m_RcvKmState; //Receiver Km State (informed by peer) ++ EncryptionKeySpec m_CurrentKey; + + private: + // Partial haicrypt configuration, consider +diff --git a/test/test_bonding.cpp b/test/test_bonding.cpp +index 11d2904f6..29eed34fd 100644 +--- a/test/test_bonding.cpp ++++ b/test/test_bonding.cpp +@@ -460,11 +460,11 @@ TEST(Bonding, Options) + + EXPECT_NE(srt_getsockflag(grp, SRTO_KMSTATE, &kms, &optsize), SRT_ERROR); + EXPECT_EQ(optsize, (int) sizeof kms); +- EXPECT_EQ(kms, int(SRT_KM_S_SECURED)); ++ EXPECT_EQ(kms, uint32_t(SRT_KM_S_SECURED)); + + EXPECT_NE(srt_getsockflag(grp, SRTO_PBKEYLEN, &kms, &optsize), SRT_ERROR); + EXPECT_EQ(optsize, (int) sizeof kms); +- EXPECT_EQ(kms, 16); ++ EXPECT_EQ(kms, uint32_t(16)); + + #ifdef ENABLE_AEAD_API_PREVIEW + EXPECT_NE(srt_getsockflag(grp, SRTO_CRYPTOMODE, &kms, &optsize), SRT_ERROR); +diff --git a/test/test_crypto.cpp b/test/test_crypto.cpp +index 466497d45..e6fb589fe 100644 +--- a/test/test_crypto.cpp ++++ b/test/test_crypto.cpp +@@ -1,11 +1,20 @@ + #include + #include + #include ++#include ++#include + + #include "gtest/gtest.h" ++#include "test_env.h" + + #include "crypto.h" + #include "handshake.h" ++#include "hcrypt_msg.h" ++#include "hcrypt.h" // Imports the CRYSPR_HAS_AESGCM definition. ++#include "socketconfig.h" ++#include "api.h" ++ ++#ifdef SRT_ENABLE_ENCRYPTION + + // processSrtMsg_KMRSP must reject malformed wire-supplied lengths before they + // reach the fixed-size stack buffer / uninitialised-read paths inside the +@@ -30,107 +39,623 @@ TEST(CryptoKMRSP, RejectsMalformedLengths) + EXPECT_EQ(crypt.processSrtMsg_KMRSP(garbage.data(), 3, srtv), srt::SRT_CMD_NONE); + } + +-#if defined(SRT_ENABLE_ENCRYPTION) && defined(ENABLE_AEAD_API_PREVIEW) +-#include "hcrypt.h" // Imports the CRYSPR_HAS_AESGCM definition. +-#include "socketconfig.h" + +-namespace srt ++#ifdef ENABLE_AEAD_API_PREVIEW ++ ++class Crypto ++ : public srt::Test + { ++protected: ++ Crypto() ++ : m_crypt(0) ++ { ++ // initialization code here ++ } + +- class Crypto +- : public ::testing::Test ++ virtual ~Crypto() + { +- protected: +- Crypto() +- : m_crypt(0) +- { +- // initialization code here +- } +- +- virtual ~Crypto() +- { +- // cleanup any pending stuff, but no exceptions allowed +- } +- +- protected: +- // SetUp() is run immediately before a test starts. +- void SetUp() override +- { +- CSrtConfig cfg; +- +- memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); +- cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; +- cfg.CryptoSecret.len = (m_pwd.size() <= (int)sizeof(cfg.CryptoSecret.str) ? m_pwd.size() : (int)sizeof(cfg.CryptoSecret.str)); +- memcpy((cfg.CryptoSecret.str), m_pwd.c_str(), m_pwd.size()); +- +- m_crypt.setCryptoSecret(cfg.CryptoSecret); +- +- // 2 = 128, 3 = 192, 4 = 256 +- cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); +- m_crypt.setCryptoKeylen(cfg.iSndCryptoKeyLen); +- +- cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_GCM; +- EXPECT_TRUE(m_crypt.init(HSD_INITIATOR, cfg, true, HaiCrypt_IsAESGCM_Supported())); +- +- const unsigned char* kmmsg = m_crypt.getKmMsg_data(0); +- const size_t km_len = m_crypt.getKmMsg_size(0); +- uint32_t kmout[72]; +- size_t kmout_len = 72; +- +- std::array km_nworder; +- NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); +- m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), kmout, kmout_len); +- } +- +- void TearDown() override +- { +- } +- +- protected: +- +- srt::CCryptoControl m_crypt; +- const std::string m_pwd = "abcdefghijk"; +- }; ++ // cleanup any pending stuff, but no exceptions allowed ++ } ++ ++protected: ++ void setup() override ++ { ++ using namespace srt; ++ ++ CSrtConfig cfg; ++ ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (m_pwd.size() <= (int)sizeof(cfg.CryptoSecret.str) ? m_pwd.size() : (int)sizeof(cfg.CryptoSecret.str)); ++ memcpy((cfg.CryptoSecret.str), m_pwd.c_str(), m_pwd.size()); ++ ++ m_crypt.setCryptoSecret(cfg.CryptoSecret); ++ ++ // 2 = 128, 3 = 192, 4 = 256 ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ m_crypt.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_GCM; ++ EXPECT_TRUE(m_crypt.init(HSD_INITIATOR, cfg, true, HaiCrypt_IsAESGCM_Supported())); + ++ const unsigned char* kmmsg = m_crypt.getKmMsg_data(0); ++ const size_t km_len = m_crypt.getKmMsg_size(0); ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; + +- // Check that destroying the buffer also frees memory units. +- TEST_F(Crypto, GCM) ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), kmout, kmout_len); ++ } ++ ++ void teardown() override + { +- if (HaiCrypt_IsAESGCM_Supported() == 0) +- GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ } ++ ++protected: ++ ++ srt::CCryptoControl m_crypt; ++ const std::string m_pwd = "abcdefghijk"; ++}; ++ ++ ++// Check that destroying the buffer also frees memory units. ++TEST_F(Crypto, GCM) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ const size_t mtu_size = 1500; ++ const size_t pld_size = 1316; ++ const size_t tag_len = 16; ++ ++ CPacket pkt; ++ pkt.allocate(mtu_size); ++ ++ const int seqno = 1; ++ const int msgno = 1; ++ const int inorder = 1; ++ const int kflg = m_crypt.getSndCryptoFlags(); ++ ++ pkt.set_seqno(seqno); ++ pkt.set_msgflags(msgno | inorder | PacketBoundaryBits(PB_SOLO) | MSGNO_ENCKEYSPEC::wrap(kflg)); ++ pkt.set_timestamp(356); ++ ++ std::iota(pkt.data(), pkt.data() + pld_size, '0'); ++ pkt.setLength(pld_size); ++ ++ EXPECT_EQ(m_crypt.encrypt(pkt), ENCS_CLEAR); ++ EXPECT_EQ(pkt.getLength(), pld_size + tag_len); ++ ++ auto pkt_enc = std::unique_ptr(pkt.clone()); ++ ++ EXPECT_EQ(m_crypt.decrypt(pkt), ENCS_CLEAR); ++ EXPECT_EQ(pkt.getLength(), pld_size); ++ ++ // Modify the payload and expect auth to fail. ++ pkt_enc->data()[10] = '5'; ++ EXPECT_EQ(m_crypt.decrypt(*pkt_enc.get()), ENCS_FAILED); ++} ++ ++// KMREQ that fails AES-KW unwrap must not downgrade a SECURED session. ++TEST_F(Crypto, KMREQ_Unwrap_Failure_Does_Not_Downgrade_Secured) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ ASSERT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // KMREQ wrapped with a different passphrase -> HAICRYPT_ERROR_WRONG_SECRET. ++ CCryptoControl other(/*socket id*/1); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ const std::string other_pwd = "completely_different_xy"; ++ cfg.CryptoSecret.len = (int)other_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, other_pwd.c_str(), other_pwd.size()); ++ other.setCryptoSecret(cfg.CryptoSecret); ++ ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ other.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_GCM; ++ ASSERT_TRUE(other.init(HSD_INITIATOR, cfg, true, HaiCrypt_IsAESGCM_Supported())); ++ ++ const unsigned char* kmmsg = other.getKmMsg_data(0); ++ const size_t km_len = other.getKmMsg_size(0); ++ ASSERT_GT(km_len, 0u); + +- const size_t mtu_size = 1500; +- const size_t pld_size = 1316; +- const size_t tag_len = 16; ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); + +- CPacket pkt; +- pkt.allocate(mtu_size); ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), ++ kmout, kmout_len); + +- const int seqno = 1; +- const int msgno = 1; +- const int inorder = 1; +- const int kflg = m_crypt.getSndCryptoFlags(); ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++} ++ ++// Malformed KMREQ (too-small payload) on a SECURED session must not ++// downgrade state. ++TEST_F(Crypto, KMREQ_MalformedSize_Does_Not_Downgrade_Secured) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ ASSERT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // Payload <= HCRYPT_MSG_KM_OFS_SALT trips the size sanity check. ++ uint32_t tiny[2] = {0, 0}; ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ EXPECT_EQ(m_crypt.processSrtMsg_KMREQ(tiny, sizeof(tiny), ++ 5, SrtVersion(1, 5, 3), kmout, kmout_len), ++ SRT_CMD_NONE); ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ EXPECT_EQ(kmout[SRT_KMR_KMSTATE], (uint32_t)SRT_KM_S_BADSECRET); ++ EXPECT_EQ(kmout_len, 1u); ++} + +- pkt.set_seqno(seqno); +- pkt.set_msgflags(msgno | inorder | PacketBoundaryBits(PB_SOLO) | MSGNO_ENCKEYSPEC::wrap(kflg)); +- pkt.set_timestamp(356); ++// KMREQ with KLEN byte zeroed -> hcryptMsg_KM_GetSekLen returns 0, ++// tripping the empty-SEK rejection. Must not downgrade SECURED state. ++TEST_F(Crypto, KMREQ_EmptySEK_Does_Not_Downgrade_Secured) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ ASSERT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // Take a structurally-valid KMREQ and zero the KLEN field. ++ const unsigned char* kmmsg = m_crypt.getKmMsg_data(0); ++ const size_t km_len = m_crypt.getKmMsg_size(0); ++ std::array patched; ++ memcpy(patched.data(), kmmsg, km_len); ++ patched[HCRYPT_MSG_KM_OFS_KLEN] = 0; ++ ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(patched.data()), km_len); ++ ++ uint32_t kmout[72] = {0}; ++ size_t kmout_len = 72; ++ EXPECT_EQ(m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, ++ 5, SrtVersion(1, 5, 3), kmout, kmout_len), ++ SRT_CMD_NONE); ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ EXPECT_EQ(kmout[SRT_KMR_KMSTATE], (uint32_t)SRT_KM_S_BADSECRET); ++ EXPECT_EQ(kmout_len, 1u); ++} + +- std::iota(pkt.data(), pkt.data() + pld_size, '0'); +- pkt.setLength(pld_size); ++// Forged KMRSP claiming any peer-failure state must not downgrade a ++// SECURED session. The dispatcher accepts KMRSPs unconditionally so each ++// peerstate branch in processSrtMsg_KMRSP is reachable off-path. ++TEST_F(Crypto, DISABLED_KMRSP_PeerFailure_Does_Not_Downgrade_Secured) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ const SRT_KM_STATE wire_peerstates[] = { ++ SRT_KM_S_BADSECRET, ++ SRT_KM_S_NOSECRET, ++ SRT_KM_S_UNSECURED, ++#ifdef ENABLE_AEAD_API_PREVIEW ++ SRT_KM_S_BADCRYPTOMODE, ++#endif ++ // An out-of-enum value drives the default ("IPE: unknown peer ++ // error state") branch in the switch. ++ (SRT_KM_STATE)99, ++ }; ++ ++ for (size_t i = 0; i < sizeof(wire_peerstates)/sizeof(wire_peerstates[0]); ++i) ++ { ++ // Reset the agent into a fully-SECURED state for each iteration. ++ m_crypt.m_RcvKmState = SRT_KM_S_SECURED; ++ m_crypt.m_SndKmState = SRT_KM_S_SECURED; ++ ++ // Wire format is network byte order; the function will HtoNLA it ++ // back. Pre-NtoHLA so srtd[SRT_KMR_KMSTATE] inside the function ++ // reads the intended peerstate. ++ uint32_t wire = (uint32_t)wire_peerstates[i]; ++ uint32_t input = 0; ++ NtoHLA(&input, &wire, 1); ++ EXPECT_EQ(m_crypt.processSrtMsg_KMRSP(&input, sizeof(input), SrtVersion(1, 5, 3)), ++ SRT_CMD_NONE); ++ ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED) ++ << "peerstate=" << (int)wire_peerstates[i] << " downgraded m_RcvKmState"; ++ EXPECT_EQ(m_crypt.m_SndKmState, SRT_KM_S_SECURED) ++ << "peerstate=" << (int)wire_peerstates[i] << " downgraded m_SndKmState"; ++ } ++} + +- EXPECT_EQ(m_crypt.encrypt(pkt), ENCS_CLEAR); +- EXPECT_EQ(pkt.getLength(), pld_size + tag_len); ++// After a forged KMREQ unwrap failure on a SECURED session, m_SndKmState ++// must also stay SECURED so sendingAllowed() keeps returning true. ++TEST_F(Crypto, KMREQ_Unwrap_Failure_Preserves_SndKmState_Secured) ++{ ++ using namespace srt; ++ ++ if (HaiCrypt_IsAESGCM_Supported() == 0) ++ GTEST_SKIP() << "The crypto service provider does not support AES GCM."; ++ ++ // setup() leaves m_SndKmState=SECURING. Simulate a fully-handshaken ++ // session where the peer's KMRSP would have moved it to SECURED. ++ m_crypt.m_SndKmState = SRT_KM_S_SECURED; ++ ASSERT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ASSERT_EQ(m_crypt.m_SndKmState, SRT_KM_S_SECURED); ++ ++ // Forge a KMREQ via a second CCryptoControl with a different passphrase. ++ CCryptoControl other(/*socket id*/1); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ const std::string other_pwd = "different_passphrase_xy"; ++ cfg.CryptoSecret.len = (int)other_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, other_pwd.c_str(), other_pwd.size()); ++ other.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ other.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_GCM; ++ ASSERT_TRUE(other.init(HSD_INITIATOR, cfg, true, HaiCrypt_IsAESGCM_Supported())); ++ ++ const unsigned char* kmmsg = other.getKmMsg_data(0); ++ const size_t km_len = other.getKmMsg_size(0); ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), ++ kmout, kmout_len); ++ ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ EXPECT_EQ(m_crypt.m_SndKmState, SRT_KM_S_SECURED); ++} + +- auto pkt_enc = std::unique_ptr(pkt.clone()); ++// Regression-test fixture that runs in default builds (CTR mode, no AEAD ++// preview required). Mirrors the setup of the Crypto fixture above. ++class CryptoCtr ++: public srt::Test ++{ ++protected: ++ CryptoCtr() : m_crypt(0) {} + +- EXPECT_EQ(m_crypt.decrypt(pkt), ENCS_CLEAR); +- EXPECT_EQ(pkt.getLength(), pld_size); ++ void setup() override ++ { ++ using namespace srt; ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (m_pwd.size() <= (int)sizeof(cfg.CryptoSecret.str) ? m_pwd.size() : (int)sizeof(cfg.CryptoSecret.str)); ++ memcpy((cfg.CryptoSecret.str), m_pwd.c_str(), m_pwd.size()); ++ ++ m_crypt.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ m_crypt.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ EXPECT_TRUE(m_crypt.init(HSD_INITIATOR, cfg, true, false)); ++ ++ const unsigned char* kmmsg = m_crypt.getKmMsg_data(0); ++ const size_t km_len = m_crypt.getKmMsg_size(0); ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), kmout, kmout_len); ++ } + +- // Modify the payload and expect auth to fail. +- pkt_enc->data()[10] = '5'; +- EXPECT_EQ(m_crypt.decrypt(*pkt_enc.get()), ENCS_FAILED); ++ void teardown() override ++ { + } + +-} // namespace srt ++protected: ++ srt::CCryptoControl m_crypt; ++ const std::string m_pwd = "abcdefghijk"; ++}; ++ ++// Regression test: validates that the legitimate SECURING -> SECURED ++// transition still happens on a valid KMREQ. Catches a future over- ++// applied SECURED-preserving guard that would block this path. ++TEST_F(CryptoCtr, InitialHandshakeReachesSecured) ++{ ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++} ++ ++// Regression test: from the SECURING initial state, a KMREQ wrapped with ++// the wrong passphrase must still drive m_RcvKmState to BADSECRET. The ++// SECURED-preserving guard must not block the initial transition. ++TEST_F(CryptoCtr, WrongPassphraseAtInitialReachesBadSecret) ++{ ++ using namespace srt; ++ ++ // Build a fresh crypter that has NOT been driven through setup's ++ // bootstrap KMREQ, so m_RcvKmState is still SECURING. ++ CCryptoControl fresh(/*socket id*/2); ++ CSrtConfig fresh_cfg; ++ memset(&fresh_cfg.CryptoSecret, 0, sizeof(fresh_cfg.CryptoSecret)); ++ fresh_cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ fresh_cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(fresh_cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ fresh.setCryptoSecret(fresh_cfg.CryptoSecret); ++ fresh_cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ fresh.setCryptoKeylen(fresh_cfg.iSndCryptoKeyLen); ++ fresh_cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(fresh.init(HSD_INITIATOR, fresh_cfg, true, false)); ++ ASSERT_NE(fresh.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // Forge a KMREQ from a peer using a different passphrase. ++ CCryptoControl other(/*socket id*/3); ++ CSrtConfig other_cfg; ++ memset(&other_cfg.CryptoSecret, 0, sizeof(other_cfg.CryptoSecret)); ++ other_cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ const std::string other_pwd = "wrong_passphrase_xy"; ++ other_cfg.CryptoSecret.len = (int)other_pwd.size(); ++ memcpy(other_cfg.CryptoSecret.str, other_pwd.c_str(), other_pwd.size()); ++ other.setCryptoSecret(other_cfg.CryptoSecret); ++ other_cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ other.setCryptoKeylen(other_cfg.iSndCryptoKeyLen); ++ other_cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(other.init(HSD_INITIATOR, other_cfg, true, false)); ++ ++ const unsigned char* kmmsg = other.getKmMsg_data(0); ++ const size_t km_len = other.getKmMsg_size(0); ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ fresh.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), ++ kmout, kmout_len); ++ ++ // The guard must NOT block this transition: from SECURING the state ++ // must reach BADSECRET so the connection can be rejected. ++ EXPECT_EQ(fresh.m_RcvKmState, SRT_KM_S_BADSECRET); ++} ++ ++// Regression test: a fresh KMREQ wrapped with the SAME passphrase ++// (i.e. legitimate key rotation) on an already-SECURED session must ++// succeed. State stays SECURED and the KMRSP returned is a full success ++// response, not a 1-word error. ++// ++// This is the highest-risk regression vector for the SECURED-preserving ++// guards: if they accidentally over-applied to the success path, key ++// rotation would silently fail and streams would eventually drop. ++TEST_F(CryptoCtr, KmRefreshOnSecuredSucceeds) ++{ ++ using namespace srt; ++ ++ ASSERT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // Build a fresh sender with the SAME passphrase. Its generated KM ++ // message will carry a different SEK but the same KEK as m_crypt, ++ // so the unwrap will succeed on m_crypt's side. This mirrors what ++ // happens when the live sender's regenCryptoKm rolls a new key. ++ CCryptoControl rotator(/*socket id*/4); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ rotator.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ rotator.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(rotator.init(HSD_INITIATOR, cfg, true, false)); ++ ++ const unsigned char* kmmsg = rotator.getKmMsg_data(0); ++ const size_t km_len = rotator.getKmMsg_size(0); ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ uint32_t kmout[72] = {0}; ++ size_t kmout_len = 72; ++ m_crypt.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), ++ kmout, kmout_len); ++ ++ // State remains SECURED across the rotation. ++ EXPECT_EQ(m_crypt.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // The success KMRSP echoes the input KM message (multi-word). A ++ // rejection path would have set kmout_len == 1 and written a single ++ // SRT_KMR_KMSTATE word. ++ EXPECT_GT(kmout_len, 1u); ++} ++ ++// Regression test: a RESPONDER-side bidirectional handshake must reach ++// SECURED on BOTH directions via the RX -> TX context clone path in ++// processSrtMsg_KMREQ. This is the most complex success transition in ++// the handler and is unguarded by design (gated by m_SndKmState == ++// SECURING && !m_hSndCrypto). ++TEST_F(CryptoCtr, ResponderHandshakeReachesSecuredViaClone) ++{ ++ using namespace srt; ++ ++ // RESPONDER doesn't create m_hSndCrypto in init(), so the ++ // SECURING + !m_hSndCrypto precondition for the clone block holds. ++ CCryptoControl responder(/*socket id*/8); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ responder.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ responder.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(responder.init(HSD_RESPONDER, cfg, true, false)); ++ ++ // Use the fixture's own (INITIATOR-side) KM message as the KMREQ ++ // payload for the responder. Same passphrase, so unwrap succeeds. ++ const unsigned char* kmmsg = m_crypt.getKmMsg_data(0); ++ const size_t km_len = m_crypt.getKmMsg_size(0); ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ responder.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, ++ SrtVersion(1, 5, 3), kmout, kmout_len); ++ ++ EXPECT_EQ(responder.m_RcvKmState, SRT_KM_S_SECURED); ++ EXPECT_EQ(responder.m_SndKmState, SRT_KM_S_SECURED); ++} ++ ++// Regression test: when the agent has no passphrase but the peer sends ++// a valid KMREQ, m_RcvKmState must transition UNSECURED -> NOSECRET. The ++// SECURED-preserving guard must not block this legitimate non-SECURED ++// transition. ++TEST_F(CryptoCtr, AgentWithoutPasswordGetsNoSecret) ++{ ++ using namespace srt; ++ ++ // Agent with no passphrase. init() leaves both states at UNSECURED. ++ CCryptoControl no_pw_agent(/*socket id*/5); ++ CSrtConfig agent_cfg; ++ memset(&agent_cfg.CryptoSecret, 0, sizeof(agent_cfg.CryptoSecret)); ++ // typ left as 0 / len 0 -> hasPassphrase() returns false. ++ no_pw_agent.setCryptoSecret(agent_cfg.CryptoSecret); ++ agent_cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ no_pw_agent.setCryptoKeylen(agent_cfg.iSndCryptoKeyLen); ++ agent_cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(no_pw_agent.init(HSD_INITIATOR, agent_cfg, true, false)); ++ ASSERT_NE(no_pw_agent.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // Peer with a valid passphrase sends KMREQ. ++ CCryptoControl peer(/*socket id*/6); ++ CSrtConfig peer_cfg; ++ memset(&peer_cfg.CryptoSecret, 0, sizeof(peer_cfg.CryptoSecret)); ++ peer_cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ peer_cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(peer_cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ peer.setCryptoSecret(peer_cfg.CryptoSecret); ++ peer_cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ peer.setCryptoKeylen(peer_cfg.iSndCryptoKeyLen); ++ peer_cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(peer.init(HSD_INITIATOR, peer_cfg, true, false)); ++ ++ const unsigned char* kmmsg = peer.getKmMsg_data(0); ++ const size_t km_len = peer.getKmMsg_size(0); ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ uint32_t kmout[72]; ++ size_t kmout_len = 72; ++ no_pw_agent.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, ++ SrtVersion(1, 5, 3), kmout, kmout_len); ++ ++ EXPECT_EQ(no_pw_agent.m_RcvKmState, SRT_KM_S_NOSECRET); ++} ++ ++// Regression test: receiving a KMRSP error report on a non-SECURED ++// session must still update state per the peer's report. The SECURED- ++// preserving guard must not block this. ++TEST_F(CryptoCtr, KmrspPeerNoSecretOnNonSecured) ++{ ++ using namespace srt; ++ ++ // Fresh crypter with passphrase but no bootstrap KMREQ processed. ++ CCryptoControl fresh(/*socket id*/7); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ fresh.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ fresh.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(fresh.init(HSD_INITIATOR, cfg, true, false)); ++ ASSERT_NE(fresh.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ // KMRSP carrying a single peerstate word = NOSECRET (peer has no PW). ++ uint32_t wire = (uint32_t)SRT_KM_S_NOSECRET; ++ uint32_t input = 0; ++ NtoHLA(&input, &wire, 1); ++ fresh.processSrtMsg_KMRSP(&input, sizeof(input), SrtVersion(1, 5, 3)); ++ ++ // Per crypto.cpp KMRSP NOSECRET branch: RX -> UNSECURED, SND -> NOSECRET. ++ EXPECT_EQ(fresh.m_RcvKmState, SRT_KM_S_UNSECURED); ++ EXPECT_EQ(fresh.m_SndKmState, SRT_KM_S_NOSECRET); ++} ++ ++// Regression test: a successful KMRSP (a KMRSP whose multi-word body ++// matches a stored sender KM message) must transition both m_RcvKmState ++// and m_SndKmState to SECURED via the success branch in ++// processSrtMsg_KMRSP. Mirror of the KMREQ success path. ++TEST_F(CryptoCtr, KmrspSuccessTransitionsToSecured) ++{ ++ using namespace srt; ++ ++ // Fresh initiator with a passphrase and the standard SND KM message ++ // built by init(), but without the bootstrap KMREQ that setup ran on ++ // m_crypt. m_SndKmState should be SECURING here. ++ CCryptoControl fresh(/*socket id*/9); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ fresh.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ fresh.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(fresh.init(HSD_INITIATOR, cfg, true, false)); ++ ASSERT_NE(fresh.m_SndKmState, SRT_KM_S_SECURED); ++ ++ // A successful KMRSP carries back the same KM payload the agent had ++ // sent. Feed the agent's own stored KM message back as the KMRSP body. ++ const unsigned char* kmmsg = fresh.getKmMsg_data(0); ++ const size_t km_len = fresh.getKmMsg_size(0); ++ ASSERT_GT(km_len, 0u); ++ ++ std::array km_nworder; ++ NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); ++ ++ fresh.processSrtMsg_KMRSP(km_nworder.data(), km_len, SrtVersion(1, 5, 3)); ++ ++ EXPECT_EQ(fresh.m_RcvKmState, SRT_KM_S_SECURED); ++ EXPECT_EQ(fresh.m_SndKmState, SRT_KM_S_SECURED); ++} ++ ++// Regression test: KMRSP carrying the UNSECURED peer-error code on a ++// non-SECURED session must transition RX -> NOSECRET, SND -> UNSECURED. ++// Mirror of the NOSECRET branch but with the opposite target mapping. ++TEST_F(CryptoCtr, KmrspPeerUnsecuredOnNonSecured) ++{ ++ using namespace srt; ++ ++ CCryptoControl fresh(/*socket id*/10); ++ CSrtConfig cfg; ++ memset(&cfg.CryptoSecret, 0, sizeof(cfg.CryptoSecret)); ++ cfg.CryptoSecret.typ = HAICRYPT_SECTYP_PASSPHRASE; ++ cfg.CryptoSecret.len = (int)m_pwd.size(); ++ memcpy(cfg.CryptoSecret.str, m_pwd.c_str(), m_pwd.size()); ++ fresh.setCryptoSecret(cfg.CryptoSecret); ++ cfg.iSndCryptoKeyLen = SrtHSRequest::SRT_PBKEYLEN_BITS::wrap(4); ++ fresh.setCryptoKeylen(cfg.iSndCryptoKeyLen); ++ cfg.iCryptoMode = CSrtConfig::CIPHER_MODE_AES_CTR; ++ ASSERT_TRUE(fresh.init(HSD_INITIATOR, cfg, true, false)); ++ ASSERT_NE(fresh.m_RcvKmState, SRT_KM_S_SECURED); ++ ++ uint32_t wire = (uint32_t)SRT_KM_S_UNSECURED; ++ uint32_t input = 0; ++ NtoHLA(&input, &wire, 1); ++ fresh.processSrtMsg_KMRSP(&input, sizeof(input), SrtVersion(1, 5, 3)); ++ ++ // Per crypto.cpp KMRSP UNSECURED branch: RX -> NOSECRET, SND -> UNSECURED. ++ EXPECT_EQ(fresh.m_RcvKmState, SRT_KM_S_NOSECRET); ++ EXPECT_EQ(fresh.m_SndKmState, SRT_KM_S_UNSECURED); ++} ++ ++#endif // AEAD + +-#endif //SRT_ENABLE_ENCRYPTION && ENABLE_AEAD_API_PREVIEW ++#endif //SRT_ENABLE_ENCRYPTION + +From 63ba8733c18fcd3750ba78b05fd5145291cf60e0 Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Fri, 5 Jun 2026 14:37:17 +0200 +Subject: [PATCH 2/8] Fixed test to match the current internal conditions + +--- + test/test_crypto.cpp | 13 +++++++++++-- + 1 file changed, 11 insertions(+), 2 deletions(-) + +diff --git a/test/test_crypto.cpp b/test/test_crypto.cpp +index e6fb589fe..c8cc2ce3b 100644 +--- a/test/test_crypto.cpp ++++ b/test/test_crypto.cpp +@@ -411,14 +411,23 @@ TEST_F(CryptoCtr, WrongPassphraseAtInitialReachesBadSecret) + std::array km_nworder; + NtoHLA(km_nworder.data(), reinterpret_cast(kmmsg), km_len); + ++ const SRT_KM_STATE prev_state = fresh.m_RcvKmState; ++ + uint32_t kmout[72]; + size_t kmout_len = 72; +- fresh.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), ++ int cmd = fresh.processSrtMsg_KMREQ(km_nworder.data(), km_len, 5, SrtVersion(1, 5, 3), + kmout, kmout_len); + + // The guard must NOT block this transition: from SECURING the state + // must reach BADSECRET so the connection can be rejected. +- EXPECT_EQ(fresh.m_RcvKmState, SRT_KM_S_BADSECRET); ++ // XXX NOTE: The behavior has been changed and now KMREQ failure is ++ // simply ignored, if it was done as update. And as we create the ++ // crypto with init, this is initialized just like through handshake, ++ // so this KMREQ is considered a KMX update. We have then the right ++ // state in the output array, but the state remains secure. ++ EXPECT_EQ(fresh.m_RcvKmState, prev_state); ++ EXPECT_EQ(cmd, SRT_CMD_NONE); ++ EXPECT_EQ(kmout[0], SRT_KM_S_BADSECRET); + } + + // Regression test: a fresh KMREQ wrapped with the SAME passphrase + +From 15a7c6f79af572b84740745339deb0cfbd881c58 Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Fri, 5 Jun 2026 15:54:13 +0200 +Subject: [PATCH 3/8] Removed rejection of KMX addressing the currently used + key + +--- + srtcore/crypto.cpp | 10 ---------- + 1 file changed, 10 deletions(-) + +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index b8d511da3..12f8b6fe7 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -234,16 +234,6 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + m_iCryptoMode = bUseGCM ? CSrtConfig::CIPHER_MODE_AES_GCM : CSrtConfig::CIPHER_MODE_AES_CTR; + HLOGC(cnlog.Debug, log << "processSrtMsg_KMREQ: created RX ENC with KeyLen=" << m_iRcvKmKeyLen); + } +- else if (m_CurrentKey != EK_NOENC) // Received at least 1 encrypted packet +- { +- // HaiCrypt_GetKeyIndex returns 0 or 1 as key index or -1 as error; so +- // adding 1 results in 1, 2 and 0 respectively, which correspond to +- // EK_EVEN, EK_ODD and EK_NOENC respectively, the latter being an error. +- int keyindex = HaiCrypt_GetKeyIndex(m_hRcvCrypto, kmdata) + 1; +- +- if (keyindex == EK_NOENC || keyindex != m_CurrentKey) +- goto Error; // Will result in BADSECRET +- } + + // We have both sides set with password, so both are pending for security + int rc = HaiCrypt_Rx_Process(m_hRcvCrypto, kmdata, bytelen, NULL, NULL, 0); + +From 80d7f419360e5ec620f77da43c9333daacfd4df5 Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Fri, 5 Jun 2026 16:49:08 +0200 +Subject: [PATCH 4/8] Simplified error value transfer + +--- + srtcore/crypto.cpp | 17 ++++++++--------- + 1 file changed, 8 insertions(+), 9 deletions(-) + +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index 12f8b6fe7..ffaf0d5bb 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -250,32 +250,32 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + } + else + { +- SRT_KM_STATE failstate; + switch(rc) + { + case HAICRYPT_ERROR_WRONG_SECRET: //Unmatched shared secret to decrypt wrapped key +- failstate = SRT_KM_S_BADSECRET; ++ failure_state = SRT_KM_S_BADSECRET; + //Send status KMRSP message to tel error + LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADSECRET"); + break; + case HAICRYPT_ERROR_CIPHER: + #ifdef ENABLE_AEAD_API_PREVIEW +- failstate = SRT_KM_S_BADCRYPTOMODE; ++ failure_state = SRT_KM_S_BADCRYPTOMODE; + #else +- failstate = SRT_KM_S_BADSECRET; // Use "bad secret" as a fallback. ++ failure_state = SRT_KM_S_BADSECRET; // Use "bad secret" as a fallback. + #endif + LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure - BADCRYPTOMODE"); + break; + case HAICRYPT_ERROR: //Other errors + default: +- failstate = SRT_KM_S_NOSECRET; ++ failure_state = SRT_KM_S_NOSECRET; + LOGC(cnlog.Warn, log << "KMREQ/rcv: (snd) Rx process failure (IPE) - NOSECRET"); + break; + } + +- if (!kmx_update) // DO NOT change any state if it was KMX update. ++ if (!kmx_update) + { +- m_RcvKmState = m_SndKmState = failstate; ++ // Only initially, set this also to SND state. ++ m_SndKmState = failure_state; + } + goto Error; + } +@@ -348,8 +348,7 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + if (!kmx_update) + { + // Set the appropriate error, if it wasn't already set before +- if (m_RcvKmState == SRT_KM_S_SECURING) +- m_RcvKmState = failure_state; ++ m_RcvKmState = failure_state; + if (bidirectional && hasPassphrase()) + { + // If the Forward KMX process has failed, the reverse-KMX process was not done at all. + +From e02c8b0785e2771103003fa16fe8a2033ff19605 Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Wed, 24 Jun 2026 12:26:18 +0200 +Subject: [PATCH 6/8] [crypto] Added rollback for changed KEK when km_unwrap + failed + +--- + haicrypt/hcrypt_ctx_rx.c | 10 +++++++++- + 1 file changed, 9 insertions(+), 1 deletion(-) + +diff --git a/haicrypt/hcrypt_ctx_rx.c b/haicrypt/hcrypt_ctx_rx.c +index 9e2dafbfa..907acdf09 100644 +--- a/haicrypt/hcrypt_ctx_rx.c ++++ b/haicrypt/hcrypt_ctx_rx.c +@@ -180,12 +180,14 @@ int hcryptCtx_Rx_ParseKM(hcrypt_Session *crypto, unsigned char *km_msg, size_t m + * Regenerate KEK if it is password derived + * and Salt or SEK length changed + */ ++ int rollback_kek = 0; + if (new_ctx.cfg.pwd_len && do_pbkdf) { + if (hcryptCtx_GenSecret(crypto, &new_ctx)) { + return(-1); + } + new_ctx.status = HCRYPT_CTX_S_SARDY; + kek_len = sek_len; /* KEK changed */ ++ rollback_kek = 1; + } + + /* Unwrap SEK(s) and set in context */ +@@ -194,7 +196,13 @@ int hcryptCtx_Rx_ParseKM(hcrypt_Session *crypto, unsigned char *km_msg, size_t m + &km_msg[HCRYPT_MSG_KM_OFS_SALT + salt_len], msglen); + + if (wrc < 0) { +- HCRYPT_LOG(LOG_WARNING, "%s", "unwrap key failed\n"); ++ HCRYPT_LOG(LOG_WARNING, "%s%s\n", "unwrap key failed - internal KEK: ", rollback_kek ? "ROLLBACK" : "unchanged"); ++ // Rollback the call to hcryptCtx_GenSecret done on the new_ctx, ++ // and restore the old secret from old ctx. GenSecret is required by ++ // km_unwrap, but only after failed call we know this should remain unchanged. ++ if (rollback_kek) { ++ hcryptCtx_GenSecret(crypto, ctx); ++ } + return(-2); //Report unmatched shared secret + } + *ctx = new_ctx; + +From e020c3bb8e04e204b36f9fb56cdcbb6756cd0d28 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Cl=C3=A9ment=20G=C3=A9rouville?= +Date: Thu, 25 Jun 2026 17:22:30 +0200 +Subject: [PATCH 7/8] Fix KMREQ buffer overflow at the right place. + +--- + srtcore/crypto.cpp | 6 ++++++ + 1 file changed, 6 insertions(+), 0 deletions(-) + +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index ffaf0d5bb..119104be1 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -147,6 +147,12 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + * Re-swap to cancel it. + */ + ++ if (bytelen % sizeof(uint32_t) != 0 || bytelen > HCRYPT_MSG_KM_MAX_SZ) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: size of the KM (" << bytelen << ") is too high, must be < " << HCRYPT_MSG_KM_MAX_SZ); ++ return SRT_CMD_NONE; ++ ++ } + // Default successful settinsga: original length and contents + w_srtlen = bytelen/sizeof(srtdata[SRT_KMR_KMSTATE]); + HtoNLA((pw_srtdata_out), srtdata, w_srtlen); + +From 78e7e144bb49b70fc1c20b44e9aa1f5705919e3e Mon Sep 17 00:00:00 2001 +From: Mikolaj Malecki +Date: Thu, 25 Jun 2026 18:19:49 +0200 +Subject: [PATCH 8/8] [core] Fixed response for rogue KMREQ message + +--- + srtcore/crypto.cpp | 51 +++++++++++++++++++++++----------------------- + 1 file changed, 26 insertions(+), 25 deletions(-) + +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index 119104be1..93b5bbe60 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -141,21 +141,6 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + const uint32_t* srtdata, size_t bytelen, int hsv, unsigned srtv, + uint32_t pw_srtdata_out[], size_t& w_srtlen) + { +- //Receiver +- /* All 32-bit msg fields swapped on reception +- * But HaiCrypt expect network order message +- * Re-swap to cancel it. +- */ +- +- if (bytelen % sizeof(uint32_t) != 0 || bytelen > HCRYPT_MSG_KM_MAX_SZ) +- { +- LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: size of the KM (" << bytelen << ") is too high, must be < " << HCRYPT_MSG_KM_MAX_SZ); +- return SRT_CMD_NONE; +- +- } +- // Default successful settinsga: original length and contents +- w_srtlen = bytelen/sizeof(srtdata[SRT_KMR_KMSTATE]); +- HtoNLA((pw_srtdata_out), srtdata, w_srtlen); + unsigned char* kmdata = reinterpret_cast(pw_srtdata_out); + + // The side that has received KMREQ is always an HSD_RESPONDER, regardless of +@@ -167,21 +152,37 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + const bool bidirectional = hsv > CUDT::HS_VERSION_UDT4; + const bool kmx_update = m_hRcvCrypto; + SRT_KM_STATE failure_state = m_KmSecret.len == 0 ? SRT_KM_S_NOSECRET : SRT_KM_S_BADSECRET; ++ bool bUseGCM = false; + +- if (!kmx_update) // Only in initial/handshake ++ // TRY-BLOCK, with THROW done by "goto Error". + { +- // If this is NOT changed anywhere later, failure_state value will be used +- m_RcvKmState = SRT_KM_S_SECURING; +- } ++ if (bytelen % sizeof(uint32_t) != 0 || bytelen > HCRYPT_MSG_KM_MAX_SZ) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMREQ: size of the KM (" << bytelen << ") is too high, must be < " << HCRYPT_MSG_KM_MAX_SZ); ++ goto Error; ++ } + +- const bool bUseGCM = +- (m_iCryptoMode == CSrtConfig::CIPHER_MODE_AUTO && kmdata[HCRYPT_MSG_KM_OFS_CIPHER] == HCRYPT_CIPHER_AES_GCM) || +- (m_iCryptoMode == CSrtConfig::CIPHER_MODE_AES_GCM); ++ // Default successful settings: original length and contents ++ w_srtlen = bytelen/sizeof(srtdata[SRT_KMR_KMSTATE]); + +- m_bUseGcm153 = srtv <= SrtVersion(1, 5, 3); ++ /* All 32-bit msg fields swapped on reception ++ * But HaiCrypt expect network order message ++ * Re-swap to cancel it. ++ */ ++ HtoNLA((pw_srtdata_out), srtdata, w_srtlen); ++ ++ if (!kmx_update) // Only in initial/handshake ++ { ++ // If this is NOT changed anywhere later, failure_state value will be used ++ m_RcvKmState = SRT_KM_S_SECURING; ++ } ++ ++ if ((m_iCryptoMode == CSrtConfig::CIPHER_MODE_AUTO && kmdata[HCRYPT_MSG_KM_OFS_CIPHER] == HCRYPT_CIPHER_AES_GCM) ++ || (m_iCryptoMode == CSrtConfig::CIPHER_MODE_AES_GCM)) ++ bUseGCM = true; ++ ++ m_bUseGcm153 = srtv <= SrtVersion(1, 5, 3); + +- // TRY-BLOCK, with THROW done by "goto Error". +- { + // INITIAL ACTIONS (first time KMREQ received): + // If encryption is on (we know that by having m_KmSecret nonempty), create + // the crypto context (if bidirectional, create for both sending and receiving). diff -Nru srt-1.5.4/debian/patches/CVE-2026-55869.patch srt-1.5.4/debian/patches/CVE-2026-55869.patch --- srt-1.5.4/debian/patches/CVE-2026-55869.patch 1970-01-01 01:00:00.000000000 +0100 +++ srt-1.5.4/debian/patches/CVE-2026-55869.patch 2026-08-15 14:21:50.000000000 +0200 @@ -0,0 +1,79 @@ +Origin: upstream, https://github.com/Haivision/srt/pull/3319 +Applied-Upstream: v1.5.6, https://github.com/Haivision/srt/releases/tag/v1.5.6 +From 0570811260a9d1e353c4ec85218fe65ab6bd2644 Mon Sep 17 00:00:00 2001 +From: Matthew Szatmary +Date: Fri, 15 May 2026 14:17:27 -0700 +Subject: [PATCH] [BUG] Validate KMRSP wire length to prevent stack overflow + +--- + srtcore/crypto.cpp | 14 ++++++++++++++ + test/test_crypto.cpp | 28 +++++++++++++++++++++++++++- + 2 files changed, 41 insertions(+), 1 deletion(-) + +diff --git a/srtcore/crypto.cpp b/srtcore/crypto.cpp +index 68d551e58..20c8ba47e 100644 +--- a/srtcore/crypto.cpp ++++ b/srtcore/crypto.cpp +@@ -370,6 +370,20 @@ int srt::CCryptoControl::processSrtMsg_KMREQ( + + int srt::CCryptoControl::processSrtMsg_KMRSP(const uint32_t* srtdata, size_t len, unsigned srtv) + { ++ // Validate the wire-supplied length before using it: ++ // - oversize would overflow the fixed-size stack buffer below; ++ // - non-word-aligned or too-small payloads are malformed by protocol and would ++ // feed uninitialised stack into downstream key-matching logic. ++ if (len > SRT_CMD_MAXSZ ++ || len < sizeof(uint32_t) ++ || (len % sizeof(uint32_t)) != 0) ++ { ++ LOGC(cnlog.Error, log << "processSrtMsg_KMRSP: malformed len " << len ++ << " (must be a non-zero multiple of " << sizeof(uint32_t) ++ << ", up to " << SRT_CMD_MAXSZ << ") - rejecting"); ++ return SRT_CMD_NONE; ++ } ++ + /* All 32-bit msg fields (if present) swapped on reception + * But HaiCrypt expect network order message + * Re-swap to cancel it. +diff --git a/test/test_crypto.cpp b/test/test_crypto.cpp +index 47b18dd1a..466497d45 100644 +--- a/test/test_crypto.cpp ++++ b/test/test_crypto.cpp +@@ -1,10 +1,36 @@ + #include + #include ++#include + + #include "gtest/gtest.h" + +-#if defined(SRT_ENABLE_ENCRYPTION) && defined(ENABLE_AEAD_API_PREVIEW) + #include "crypto.h" ++#include "handshake.h" ++ ++// processSrtMsg_KMRSP must reject malformed wire-supplied lengths before they ++// reach the fixed-size stack buffer / uninitialised-read paths inside the ++// function. Built into the library unconditionally, so this test runs ++// regardless of SRT_ENABLE_ENCRYPTION. ++TEST(CryptoKMRSP, RejectsMalformedLengths) ++{ ++ srt::CCryptoControl crypt(0); ++ std::vector garbage(SRT_CMD_MAXSZ, 0); ++ const unsigned srtv = srt::SrtVersion(1, 5, 3); ++ ++ // Oversize: would overflow uint32_t srtd[SRTDATA_MAXSIZE]. ++ EXPECT_EQ(crypt.processSrtMsg_KMRSP(garbage.data(), SRT_CMD_MAXSZ + sizeof(uint32_t), srtv), ++ srt::SRT_CMD_NONE); ++ ++ // Non-word-aligned: silently drops bytes and risks misinterpretation. ++ EXPECT_EQ(crypt.processSrtMsg_KMRSP(garbage.data(), 7, srtv), srt::SRT_CMD_NONE); ++ ++ // Empty / under-a-word: HtoNLA writes nothing and downstream code would read ++ // uninitialised stack from srtd[]. ++ EXPECT_EQ(crypt.processSrtMsg_KMRSP(garbage.data(), 0, srtv), srt::SRT_CMD_NONE); ++ EXPECT_EQ(crypt.processSrtMsg_KMRSP(garbage.data(), 3, srtv), srt::SRT_CMD_NONE); ++} ++ ++#if defined(SRT_ENABLE_ENCRYPTION) && defined(ENABLE_AEAD_API_PREVIEW) + #include "hcrypt.h" // Imports the CRYSPR_HAS_AESGCM definition. + #include "socketconfig.h" + diff -Nru srt-1.5.4/debian/patches/series srt-1.5.4/debian/patches/series --- srt-1.5.4/debian/patches/series 2022-07-02 11:10:44.000000000 +0200 +++ srt-1.5.4/debian/patches/series 2026-08-15 14:22:59.000000000 +0200 @@ -1,2 +1,4 @@ 001-multiarch-rpath.patch test_failures.patch +CVE-2026-55869.patch +CVE-2026-55868.patch