From 04645d62b5193b0cfb8d40d7f8e89ae72bbc3b38 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 3 Aug 2026 16:28:02 -0700 Subject: [PATCH 1/4] ssh.h: document the WS_CallbackUserAuth contract - WOLFSSH_USERAUTH_SUCCESS is 0, the same value as WS_SUCCESS and the C "no error" idiom, so a bare "return 0", a forwarded WS_SUCCESS, or a fall-through default of 0 silently authenticates the client. - Spell out that the callback must fail closed: return WOLFSSH_USERAUTH_FAILURE for any authType or code path it does not explicitly handle. - Note that for WOLFSSH_USERAUTH_PUBLICKEY the library verifies the signature but not the key's authorization, so the callback has to check the offered key against the user's authorized keys. Issue: F-6815 --- wolfssh/ssh.h | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index 57e8f2452..d28db842f 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -439,6 +439,27 @@ typedef struct WS_UserAuthData { } sf; } WS_UserAuthData; +/* User Authentication Callback + * + * A server-side callback that decides whether to authenticate a client. + * Return WOLFSSH_USERAUTH_SUCCESS only on a positive authentication + * decision. WOLFSSH_USERAUTH_PARTIAL_SUCCESS reports that one factor of + * a multi-method authentication passed, WOLFSSH_USERAUTH_SUCCESS_ANOTHER + * reports that a keyboard-interactive round passed and asks for the next + * round, WOLFSSH_USERAUTH_WOULD_BLOCK asks for the request to be + * retried, and WOLFSSH_USERAUTH_REJECTED is a hard rejection that ends + * the session; any other value is treated as an ordinary failure. + * + * WARNING: WOLFSSH_USERAUTH_SUCCESS has the value 0, the same as + * WS_SUCCESS and the C "no error" idiom. A bare "return 0;", a forwarded + * WS_SUCCESS from a helper, or a fall-through default of 0 silently + * authenticates the client. Return WOLFSSH_USERAUTH_FAILURE for any + * authType or code path the callback does not explicitly handle. + * + * For WOLFSSH_USERAUTH_PUBLICKEY, the callback must check the offered + * public key against the user's authorized keys; returning success on an + * unchecked key authorizes an attacker-supplied key. The library verifies + * the signature, not the key's authorization. */ typedef int (*WS_CallbackUserAuth)(byte authType, WS_UserAuthData* authData, void* ctx); WOLFSSH_API void wolfSSH_SetUserAuth(WOLFSSH_CTX* ctx, WS_CallbackUserAuth cb); From 30a12a9b6583a1a50d76b8e6d76414aed7129891 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 3 Aug 2026 16:28:02 -0700 Subject: [PATCH 2/4] ssh.h: document the WS_CallbackPublicKeyCheck contract - The callback is the client's only defense against a man in the middle, and 0 accepts the server host key, so a stub that defaults to "return 0" trusts whatever key is presented. - State that the callback must match the key against a trust store, and point at ClientPublicKeyCheck() in the examples. - Record that with no callback registered the host key is rejected with WS_PUBKEY_REJECTED_E. Issue: F-6976 --- wolfssh/ssh.h | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/wolfssh/ssh.h b/wolfssh/ssh.h index d28db842f..aae535bf2 100644 --- a/wolfssh/ssh.h +++ b/wolfssh/ssh.h @@ -477,7 +477,17 @@ WOLFSSH_API void wolfSSH_SetUserAuthResultCtx(WOLFSSH* ssh, void* userAuthResultCtx); WOLFSSH_API void* wolfSSH_GetUserAuthResultCtx(WOLFSSH* ssh); -/* Public Key Check Callback */ +/* Public Key Check Callback + * + * A client-side callback that decides whether to trust the server's host + * key, the client's only defense against a man-in-the-middle. Return 0 to + * accept the key; return non-zero to reject it and fail the key exchange. + * + * WARNING: 0 accepts, so a stub that defaults to "return 0;" accepts any + * server host key and defeats MITM protection. The callback must match + * the key against a trust store, e.g. a known-hosts list; see + * ClientPublicKeyCheck() in the examples. If no callback is registered, + * the host key is rejected (WS_PUBKEY_REJECTED_E). */ typedef int (*WS_CallbackPublicKeyCheck)(const byte* publicKey, word32 publicKeySz, void* ctx); WOLFSSH_API void wolfSSH_CTX_SetPublicKeyCheck(WOLFSSH_CTX* ctx, From 710fa4ed161e55193213ecb2cd7621d158ebf868 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 3 Aug 2026 16:28:02 -0700 Subject: [PATCH 3/4] internal: validate the ECC curve name in user auth - DoUserAuthRequestEcc() skipped the curve name in the public key blob and imported the point with wc_ecc_import_x963(), which picks the curve from the point length, so the key did not have to be on the curve the declared algorithm names. - Derive the curve from pk->publicKeyType with NameToId() and wcPrimeForId(), require the blob's curve name to equal PrimeNameForId() for that id, and import with wc_ecc_import_x963_ex() pinned to that curve. - The import's error check sat outside the success guard, so it rewrote any earlier error as WS_CRYPTO_FAILED. Scope it to the import itself so the parse and algorithm-match errors keep their own codes. - Add test_EccUserAuthCurveMismatch, which offers a blob naming one curve under another algorithm and expects the request to fail. Issue: F-6979 --- src/internal.c | 37 +++++-- tests/unit.c | 275 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index 8680bf5a7..4f4989745 100644 --- a/src/internal.c +++ b/src/internal.c @@ -8507,6 +8507,8 @@ static int DoUserAuthRequestEcc(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, word32 sz, qSz, rSz, sSz; word32 i = 0, asnSigSz = ECDSA_ASN_SIG_SZ; int ret = WS_SUCCESS; + int primeId = ECC_CURVE_INVALID; + byte keyId = ID_NONE; ecc_key *key_ptr = NULL; byte* asnSig = NULL; #ifndef WOLFSSH_SMALL_STACK @@ -8559,26 +8561,43 @@ static int DoUserAuthRequestEcc(WOLFSSH* ssh, WS_UserAuthData_PublicKey* pk, } } + /* Derive the curve from the declared algorithm, not from the blob. */ + if (ret == WS_SUCCESS) { + keyId = NameToId((const char*)pk->publicKeyType, pk->publicKeyTypeSz); + primeId = wcPrimeForId(keyId); + if (primeId == ECC_CURVE_INVALID) { + ret = WS_INVALID_PRIME_CURVE; + } + } + if (ret == WS_SUCCESS) ret = GetSize(&curveNameSz, pk->publicKey, pk->publicKeySz, &i); + /* The curve name (RFC 5656 section 3.1) in the blob must match the + * curve of the declared algorithm. */ if (ret == WS_SUCCESS) { + const char* primeName = PrimeNameForId(keyId); + curveName = pk->publicKey + i; - WOLFSSH_UNUSED(curveName); - /* Not used at the moment, hush the compiler. */ i += curveNameSz; - ret = GetSize(&qSz, pk->publicKey, pk->publicKeySz, &i); + if (curveNameSz != (word32)WSTRLEN(primeName) + || WMEMCMP(curveName, primeName, curveNameSz) != 0) { + WLOG(WS_LOG_DEBUG, + "Public Key's curve name does not match its type"); + ret = WS_INVALID_PRIME_CURVE; + } } + if (ret == WS_SUCCESS) + ret = GetSize(&qSz, pk->publicKey, pk->publicKeySz, &i); + if (ret == WS_SUCCESS) { q = pk->publicKey + i; i += qSz; - ret = wc_ecc_import_x963(q, qSz, key_ptr); - } - - if (ret != 0) { - WLOG(WS_LOG_DEBUG, "Could not decode public key"); - ret = WS_CRYPTO_FAILED; + if (wc_ecc_import_x963_ex(q, qSz, key_ptr, primeId) != 0) { + WLOG(WS_LOG_DEBUG, "Could not decode public key"); + ret = WS_CRYPTO_FAILED; + } } if (ret == WS_SUCCESS) { diff --git a/tests/unit.c b/tests/unit.c index 3862ec27f..991041e5b 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -6971,6 +6971,273 @@ static int test_SendUserAuthFailure_emptyMethods(void) } +#if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) + +/* Append an SSH string (uint32 BE length followed by the bytes) at *pIdx. */ +static void AppendSshString(byte* buf, word32* pIdx, + const void* data, word32 dataSz) +{ + PutU32BE(buf + *pIdx, dataSz); + *pIdx += UINT32_SZ; + if (dataSz > 0) { + WMEMCPY(buf + *pIdx, data, dataSz); + } + *pIdx += dataSz; +} + +/* Verify DoUserAuthRequestEcc() binds the curve name carried in the public key + * blob to the declared public key algorithm (RFC 5656 Section 3.1: the key blob + * is string "ecdsa-sha2-[identifier]" || string [identifier] || string Q, so + * the two identifiers must agree). + * + * Without that binding the blob's curve name is discarded and the point is + * imported with the curve inferred from its own size, so a request declaring + * ecdsa-sha2-nistp256 while carrying a P-384 curve name, a P-384 point, and a + * P-384 signature over the SHA-256 digest verifies and authenticates. The + * declared algorithm picks the hash, and signing a 32-byte digest with a P-384 + * key is valid ECDSA, so nothing else in the path rejects it. + * + * Four cases, each on a fresh WOLFSSH: + * 1. Control: P-256 key, curve name "nistp256". Must authenticate - proves + * the message layout and signing recipe are right, so the mismatch cases + * fail for the intended reason, and that the check does not break the + * happy path. + * 2. Mismatch: P-384 key and curve name "nistp384", algorithm still + * ecdsa-sha2-nistp256. Same length as "nistp256", so this exercises the + * content arm of the curve name check. Must be rejected with + * USERAUTH_FAILURE. + * 3. Point mismatch: curve name "nistp256" agrees with the algorithm but + * the point and signature are P-384. The name check passes; only the + * import pinned to the declared curve (wc_ecc_import_x963_ex) rejects + * it. Must be rejected with USERAUTH_FAILURE. + * 4. Truncated name: curve name "nistp" with a P-256 key, exercising the + * length arm of the curve name check. Must be rejected with + * USERAUTH_FAILURE. + * + * All cases assert ret == WS_SUCCESS (the connection stays up either way) and + * idx == len (the whole payload is consumed). */ +static int test_EccUserAuthCurveMismatch(void) +{ + static const char algoName[] = "ecdsa-sha2-nistp256"; + static const struct { + const char* curveName; + int keySz; + int curveId; + int expectAuth; + const char* label; + } cases[] = { + { "nistp256", 32, ECC_SECP256R1, 1, "matching curve" }, + { "nistp384", 48, ECC_SECP384R1, 0, "curve mismatch" }, + { "nistp256", 48, ECC_SECP384R1, 0, "matching name, wrong point" }, + { "nistp", 32, ECC_SECP256R1, 0, "truncated curve name" }, + }; + int result = 0; + int i; + + for (i = 0; i < (int)(sizeof(cases)/sizeof(cases[0])); i++) { + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + WC_RNG rng; + ecc_key key; + byte buf[512]; + byte toSign[512]; + byte q[133]; + byte der[140]; + byte r[66]; + byte s[66]; + byte digest[WC_MAX_DIGEST_SIZE]; + word32 algoSz = (word32)WSTRLEN(algoName); + word32 curveSz = (word32)WSTRLEN(cases[i].curveName); + word32 qSz = (word32)sizeof(q); + word32 derSz = (word32)sizeof(der); + word32 rSz = (word32)sizeof(r); + word32 sSz = (word32)sizeof(s); + word32 len = 0, idx = 0, blobSz, sigBlobSz, tsSz; + byte rPad, sPad; + int rngReady = 0, keyReady = 0; + int ret; + + WMEMSET(&rng, 0, sizeof(rng)); + WMEMSET(&key, 0, sizeof(key)); + WMEMSET(digest, 0, sizeof(digest)); + + if (wc_ecc_init(&key) != 0) { + result = -776; + break; + } + keyReady = 1; + if (wc_InitRng(&rng) != 0) { + result = -777; + goto caseDone; + } + rngReady = 1; + if (wc_ecc_make_key_ex(&rng, cases[i].keySz, &key, + cases[i].curveId) != 0) { + result = -778; + goto caseDone; + } + if (wc_ecc_export_x963(&key, q, &qSz) != 0) { + result = -779; + goto caseDone; + } + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) { + result = -780; + goto caseDone; + } + wolfSSH_SetIOSend(ctx, CaptureIoSendAuthSvc); + wolfSSH_SetUserAuth(ctx, UnitAuthAlwaysSucceed); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { + result = -781; + goto caseDone; + } + /* The digest recipe below assumes an empty session id. */ + if (ssh->sessionIdSz != 0) { + result = -782; + goto caseDone; + } + + s_authSvcCaptureSz = 0; + s_authSvcSendCount = 0; + WMEMSET(s_authSvcCapture, 0, sizeof(s_authSvcCapture)); + + /* Fields 1-6, the region covered by the signature. */ + AppendSshString(buf, &len, "user", 4); + AppendSshString(buf, &len, "ssh-connection", 14); + AppendSshString(buf, &len, "publickey", 9); + buf[len++] = 1; /* has signature */ + AppendSshString(buf, &len, algoName, algoSz); + + /* Key blob: string(algo) || string(curve name) || string(Q). */ + blobSz = (UINT32_SZ + algoSz) + (UINT32_SZ + curveSz) + + (UINT32_SZ + qSz); + PutU32BE(buf + len, blobSz); + len += UINT32_SZ; + AppendSshString(buf, &len, algoName, algoSz); + AppendSshString(buf, &len, cases[i].curveName, curveSz); + AppendSshString(buf, &len, q, qSz); + + /* Signed data: uint32(sessionIdSz) || sessionId || msg id || fields + * 1-6. sessionIdSz is 0, so the first two collapse to four zeros. */ + PutU32BE(toSign, 0); + tsSz = UINT32_SZ; + toSign[tsSz++] = MSGID_USERAUTH_REQUEST; + WMEMCPY(toSign + tsSz, buf, len); + tsSz += len; + + /* The declared algorithm selects SHA-256 for both cases. */ + if (wc_Hash(WC_HASH_TYPE_SHA256, toSign, tsSz, + digest, WC_SHA256_DIGEST_SIZE) != 0) { + result = -783; + goto caseDone; + } + if (wc_ecc_sign_hash(digest, WC_SHA256_DIGEST_SIZE, der, &derSz, + &rng, &key) != 0) { + result = -784; + goto caseDone; + } + if (wc_ecc_sig_to_rs(der, derSz, r, &rSz, s, &sSz) != 0) { + result = -785; + goto caseDone; + } + + /* r and s come back minimal big-endian; pad to keep the mpints + * positive, as SendUserAuthRequest() does. */ + rPad = (r[0] & 0x80) ? 1 : 0; + sPad = (s[0] & 0x80) ? 1 : 0; + sigBlobSz = (UINT32_SZ * 2) + rSz + rPad + sSz + sPad; + + /* Field 7: string(string(algo) || string(string(r) || string(s))). */ + PutU32BE(buf + len, (UINT32_SZ + algoSz) + (UINT32_SZ + sigBlobSz)); + len += UINT32_SZ; + AppendSshString(buf, &len, algoName, algoSz); + PutU32BE(buf + len, sigBlobSz); + len += UINT32_SZ; + PutU32BE(buf + len, rSz + rPad); + len += UINT32_SZ; + if (rPad) + buf[len++] = 0; + WMEMCPY(buf + len, r, rSz); + len += rSz; + PutU32BE(buf + len, sSz + sPad); + len += UINT32_SZ; + if (sPad) + buf[len++] = 0; + WMEMCPY(buf + len, s, sSz); + len += sSz; + + ret = wolfSSH_TestDoUserAuthRequest(ssh, buf, len, &idx); + + if (ret != WS_SUCCESS) { + printf("EccUserAuthCurveMismatch[%s]: ret=%d expected" + " WS_SUCCESS\n", cases[i].label, ret); + result = -786; + goto caseDone; + } + if (idx != len) { + printf("EccUserAuthCurveMismatch[%s]: idx=%u expected %u\n", + cases[i].label, idx, len); + result = -787; + goto caseDone; + } + + if (cases[i].expectAuth) { + if (s_authSvcSendCount != 0) { + printf("EccUserAuthCurveMismatch[%s]: expected 0 sends," + " got %u\n", cases[i].label, s_authSvcSendCount); + result = -788; + goto caseDone; + } + if (ssh->clientState != CLIENT_USERAUTH_DONE) { + printf("EccUserAuthCurveMismatch[%s]: not authenticated\n", + cases[i].label); + result = -789; + goto caseDone; + } + } + else { + if (s_authSvcSendCount != 1) { + printf("EccUserAuthCurveMismatch[%s]: expected 1 send," + " got %u\n", cases[i].label, s_authSvcSendCount); + result = -790; + goto caseDone; + } + if (CaptureMsgId(s_authSvcCapture, s_authSvcCaptureSz) + != MSGID_USERAUTH_FAILURE) { + printf("EccUserAuthCurveMismatch[%s]: expected" + " USERAUTH_FAILURE\n", cases[i].label); + result = -791; + goto caseDone; + } + if (ssh->clientState == CLIENT_USERAUTH_DONE) { + printf("EccUserAuthCurveMismatch[%s]: authenticated with a" + " mismatched curve\n", cases[i].label); + result = -792; + goto caseDone; + } + } + +caseDone: + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + if (rngReady) + wc_FreeRng(&rng); + if (keyReady) + wc_ecc_free(&key); + if (result != 0) + break; + } + + return result; +} + +#endif /* !WOLFSSH_NO_ECDSA_SHA2_NISTP256 && !WOLFSSH_NO_ECDSA_SHA2_NISTP384 */ + + #if !defined(WOLFSSH_NO_RSA) /* 2048-bit RSA private key (PKCS#1 DER). @@ -13450,6 +13717,14 @@ int wolfSSH_UnitTest(int argc, char** argv) (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; +#if !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP256) && \ + !defined(WOLFSSH_NO_ECDSA_SHA2_NISTP384) + unitResult = test_EccUserAuthCurveMismatch(); + printf("EccUserAuthCurveMismatch: %s\n", + (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; +#endif + unitResult = test_MaxAuthAttempts(); printf("MaxAuthAttempts: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; From 272c1da1eb86bb30b56edafb82641cf5db9a6a69 Mon Sep 17 00:00:00 2001 From: John Safranek Date: Mon, 3 Aug 2026 16:28:02 -0700 Subject: [PATCH 4/4] tests: add test_ByteHighwater - The byte-count branch of HighwaterCheck() had no coverage. Exercise the boundary at the mark, the once-per-epoch flag that keeps the callback from firing a second time, the receive side, and a mark of 0 disabling the check. Issue: F-6978 --- tests/unit.c | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tests/unit.c b/tests/unit.c index 991041e5b..f086716d4 100644 --- a/tests/unit.c +++ b/tests/unit.c @@ -3352,6 +3352,99 @@ static int test_MsgHighwater(void) return result; } +/* Sibling of test_MsgHighwater for the byte-count branch of HighwaterCheck + * (RFC 4253 Section 9 keystream/IV bound). Covers: + * - Threshold boundary: mark-1 does not fire, mark fires (>= not >) + * - Callback fires exactly once per epoch (highwaterFlag gates re-firing) + * - Receive side fires independently after an epoch reset + * - highwaterMark == 0 disables the byte check */ +static int test_ByteHighwater(void) +{ + WOLFSSH_CTX* ctx = NULL; + WOLFSSH* ssh = NULL; + HwTestCtx hc; + int result = 0; + + ctx = wolfSSH_CTX_new(WOLFSSH_ENDPOINT_SERVER, NULL); + if (ctx == NULL) + return -820; + + WMEMSET(&hc, 0, sizeof(hc)); + wolfSSH_SetHighwaterCb(ctx, 1024, HwTestCb); + + ssh = wolfSSH_new(ctx); + if (ssh == NULL) { + result = -821; + goto done; + } + wolfSSH_SetHighwaterCtx(ssh, &hc); + /* Disable the packet-count branch so only the byte branch is under + * test. */ + wolfSSH_SetMsgHighwater(ssh, 0); + + if (wolfSSH_GetHighwater(ssh) != 1024) { + result = -822; + goto done; + } + + /* One byte under the mark on both sides: must not fire. */ + ssh->txCount = 1023; + ssh->rxCount = 1023; + if (wolfSSH_TestHighwaterCheck(ssh, WOLFSSH_HWSIDE_TRANSMIT) != WS_SUCCESS + || hc.count != 0) { + result = -823; + goto done; + } + + /* At the mark: fires, with the transmit side reported. */ + ssh->txCount = 1024; + if (wolfSSH_TestHighwaterCheck(ssh, WOLFSSH_HWSIDE_TRANSMIT) != WS_SUCCESS + || hc.count != 1 + || hc.lastSide != WOLFSSH_HWSIDE_TRANSMIT) { + result = -824; + goto done; + } + + /* Flag-gated: more bytes in the same epoch must not re-fire. */ + ssh->txCount = 4096; + if (wolfSSH_TestHighwaterCheck(ssh, WOLFSSH_HWSIDE_TRANSMIT) != WS_SUCCESS + || hc.count != 1) { + result = -825; + goto done; + } + + /* Fresh key epoch (highwaterFlag and tx/rxCount are reset by + * DoNewKeys/SendNewKeys): the receive side fires on its own. */ + ssh->highwaterFlag = 0; + ssh->txCount = 0; + ssh->rxCount = 1024; + if (wolfSSH_TestHighwaterCheck(ssh, WOLFSSH_HWSIDE_RECEIVE) != WS_SUCCESS + || hc.count != 2 + || hc.lastSide != WOLFSSH_HWSIDE_RECEIVE) { + result = -826; + goto done; + } + + /* mark == 0 disables the byte check entirely. */ + if (wolfSSH_SetHighwater(ssh, 0) != WS_SUCCESS) { + result = -827; + goto done; + } + ssh->highwaterFlag = 0; + ssh->txCount = 0xFFFFFFFFu; + ssh->rxCount = 0xFFFFFFFFu; + if (wolfSSH_TestHighwaterCheck(ssh, WOLFSSH_HWSIDE_TRANSMIT) != WS_SUCCESS + || hc.count != 2) { + result = -828; + goto done; + } + +done: + wolfSSH_free(ssh); + wolfSSH_CTX_free(ctx); + return result; +} + static int test_DoChannelSuccess(void) { WOLFSSH_CTX* ctx = NULL; @@ -13702,6 +13795,10 @@ int wolfSSH_UnitTest(int argc, char** argv) printf("MsgHighwater: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); testResult = testResult || unitResult; + unitResult = test_ByteHighwater(); + printf("ByteHighwater: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED")); + testResult = testResult || unitResult; + unitResult = test_DoUserAuthRequest_serviceName(); printf("DoUserAuthRequest_serviceName: %s\n", (unitResult == 0 ? "SUCCESS" : "FAILED"));