From 1f0192c22a88bf3a30af4ecef8ec375523942e4c Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 13:59:43 +0800 Subject: [PATCH 01/15] env --- .ide/Dockerfile | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .ide/Dockerfile diff --git a/.ide/Dockerfile b/.ide/Dockerfile new file mode 100644 index 0000000..ad01a17 --- /dev/null +++ b/.ide/Dockerfile @@ -0,0 +1,5 @@ +# .ide/Dockerfile + +FROM cnbcool/default-dev-env:latest + +RUN apt-get update && apt-get install -y g++ libssl-dev strace make libtool autoconf automake From 994ef4ac59e39bde29a3795da89cfb61e37734a8 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:07:07 +0800 Subject: [PATCH 02/15] dev script --- build.sh | 12 ++++++++++++ run.sh | 7 +++++++ 2 files changed, 19 insertions(+) create mode 100755 build.sh create mode 100755 run.sh diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..18f68ed --- /dev/null +++ b/build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e + +make -f makefile.dist + +./configure + +# ./configure WITH_LTO=yes + +make + diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..e4ff19b --- /dev/null +++ b/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -e + +chmod +x ./src/tssh + +./src/tssh -p 22 -l cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg -d cnb.space From 404dd02fd86a3f8df0098ce227d102908babb2fa Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:13:00 +0800 Subject: [PATCH 03/15] run script --- .env | 2 ++ run.sh | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..6cb38b3 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +USERNAME="cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg" +HOST="cnb.space" diff --git a/run.sh b/run.sh index e4ff19b..d6e205c 100755 --- a/run.sh +++ b/run.sh @@ -4,4 +4,7 @@ set -e chmod +x ./src/tssh -./src/tssh -p 22 -l cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg -d cnb.space +source .env +set +a + +./src/tssh -p 22 -l $USERNAME -d $HOST From 435a69feea35aa073578589ed86869dce302ba72 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:19:25 +0800 Subject: [PATCH 04/15] =?UTF-8?q?fix:=20DH=20=E5=AF=86=E9=92=A5=E4=BA=A4?= =?UTF-8?q?=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Tssh.cpp | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/src/Tssh.cpp b/src/Tssh.cpp index dde4406..6187546 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -744,28 +744,13 @@ namespace tssh{ get(dhReplyPacket), get(dhReplyPacket)); checkServerSignature(); - - if(get(dhReplyPacket) < buffCopy.size()){ - uint32_t next { charToUint32(buffCopy.data() + get(dhReplyPacket) + - sizeof(uint32_t))}; - - TRACE("* Next Len: " + to_string(next)); - - try{ - if(buffCopy.at(get(dhReplyPacket) + 2*sizeof(uint32_t) + sizeof(uint8_t)) - != SSH_MSG_NEWKEYS ) { - trace("Unexpected Packet Type: ", &buffCopy, 0, 0, - charToUint32(buffCopy.data()) + sizeof(uint32_t)); - throw InetException(string("checkServerDhReply: Invalid Packet type, expected: ") + - to_string(SSH_MSG_NEWKEYS) + " - Received: " + - to_string(buffCopy[get(dhReplyPacket) + - sizeof(uint32_t) + sizeof(uint8_t)])); - } - }catch(...){ - throw InetException("checkServerDhReply: b : Invalid index."); - } - } else { - throw InetException("checkServerDhReply: SSH_MSG_NEWKEYS packet not received."); + + // Read SSH_MSG_NEWKEYS packet (separate read after DH Reply) + readSsh(); + if(buffCopy.at(PACKET_TYPE_OFFSET) != SSH_MSG_NEWKEYS) { + throw InetException(string("checkServerDhReply: Invalid Packet type, expected: ") + + to_string(SSH_MSG_NEWKEYS) + " - Received: " + + to_string(buffCopy.at(PACKET_TYPE_OFFSET))); } packetsRcvCount++; From 962b792dd7f5df25fedc663b53cb355cd8efd561 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:40:34 +0800 Subject: [PATCH 05/15] =?UTF-8?q?fix:=20HMAC=20=E6=A0=A1=E9=AA=8C=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/Tssh.hpp | 3 +++ src/Tssh.cpp | 28 +++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/include/Tssh.hpp b/include/Tssh.hpp index 4ac921c..27a9527 100644 --- a/include/Tssh.hpp +++ b/include/Tssh.hpp @@ -301,6 +301,9 @@ namespace tssh{ const std::string& getServerId(void) const noexcept; const std::string& getClientId(void) const noexcept; void getStatistics(void) const noexcept; + uint8_t getLastPacketType(void) const noexcept; + uint32_t getSndCount(void) const noexcept; + uint32_t getRcvCount(void) const noexcept; void addHeader(uint8_t packetType, std::vector& buff) const anyexcept; void sendWithHeader(std::vector& buff, diff --git a/src/Tssh.cpp b/src/Tssh.cpp index 6187546..e3df53d 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -300,6 +300,18 @@ namespace tssh{ cerr << "* Received " << packetsRcvCount << " ssh packets." << '\n' << "* Sent " << packetsSndCount << " ssh packets." << '\n'; } + + uint8_t SshTransport::getLastPacketType(void) const noexcept{ + return buffCopy.at(PACKET_TYPE_OFFSET); + } + + uint32_t SshTransport::getSndCount(void) const noexcept{ + return packetsSndCount; + } + + uint32_t SshTransport::getRcvCount(void) const noexcept{ + return packetsRcvCount; + } void SshTransport::readSsh(void) anyexcept{ packetsRcvCount++; @@ -745,15 +757,6 @@ namespace tssh{ checkServerSignature(); - // Read SSH_MSG_NEWKEYS packet (separate read after DH Reply) - readSsh(); - if(buffCopy.at(PACKET_TYPE_OFFSET) != SSH_MSG_NEWKEYS) { - throw InetException(string("checkServerDhReply: Invalid Packet type, expected: ") + - to_string(SSH_MSG_NEWKEYS) + " - Received: " + - to_string(buffCopy.at(PACKET_TYPE_OFFSET))); - } - - packetsRcvCount++; haveKeys = true; } @@ -1615,6 +1618,13 @@ namespace tssh{ readSsh(); checkServerDhReply(); + + // Read SSH_MSG_NEWKEYS packet (separate from DH Reply) + readSsh(); + if(getLastPacketType() != SSH_MSG_NEWKEYS) + throw InetException(string("getShell: Invalid Packet type after DH Reply, expected: ") + + to_string(SSH_MSG_NEWKEYS) + " - Received: " + + to_string(getLastPacketType())); createKeys(currentHashCLen); addHeader(SSH_MSG_NEWKEYS, msg); From 48314ec1cdf3803f11a39917d542de9f6aca8bc4 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:59:32 +0800 Subject: [PATCH 06/15] run script --- .env | 4 ++-- run.sh | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 6cb38b3..625bf19 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -USERNAME="cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg" -HOST="cnb.space" +TSSH_USERNAME="cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg" +TSSH_HOST="cnb.space" diff --git a/run.sh b/run.sh index d6e205c..05cfabe 100755 --- a/run.sh +++ b/run.sh @@ -1,10 +1,11 @@ #!/bin/bash -set -e +set -eo chmod +x ./src/tssh source .env set +a -./src/tssh -p 22 -l $USERNAME -d $HOST +# ./src/tssh -p 22 -l "$TSSH_USERNAME" -d "$TSSH_HOST" +./src/tssh -p 22 -l "$TSSH_USERNAME" "$TSSH_HOST" From b90e168976440868019fdb019ab8697a6f6cf117 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 14:59:45 +0800 Subject: [PATCH 07/15] feat: none --- src/Tssh.cpp | 69 ++++++++++++++++++++-------------------------------- 1 file changed, 27 insertions(+), 42 deletions(-) diff --git a/src/Tssh.cpp b/src/Tssh.cpp index e3df53d..fde5a9e 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -91,6 +91,7 @@ namespace tssh{ static const char *SSH_USERAUTH_STRING { "ssh-userauth" }; static const char *SSH_CONNECT_STRING { "ssh-connection" }; + static const char *SSH_NONE_AUTH_REQ { "none" }; static const char *SSH_PUBKEY_AUTH_REQ { "publickey" }; static const char *SSH_PASSWD_SPEC { "password" }; static const char *SSH_KEYB_INTER_SPEC { "keyboard-interactive" }; @@ -1062,6 +1063,7 @@ namespace tssh{ void SshConnection::connectionLoop(void) anyexcept{ bool again { true }, + noneAuth { false }, pubKeyAuth { false }, password { false }, keybInter { false }; @@ -1078,7 +1080,8 @@ namespace tssh{ SSH_MSG_USERAUTH_INFO_REQUEST, SSH_MSG_USERAUTH_FAILURE}}, {SSH_MSG_USERAUTH_SUCCESS, {SSH_MSG_USERAUTH_INFO_REQUEST, - SSH_MSG_USERAUTH_FAILURE}}, + SSH_MSG_USERAUTH_FAILURE, + SSH_MSG_SERVICE_ACCEPT}}, {SSH_MSG_CHANNEL_OPEN_CONFIRMATION, {SSH_MSG_USERAUTH_SUCCESS}}, {SSH_MSG_CHANNEL_WINDOW_ADJUST, {SSH_MSG_CHANNEL_OPEN_CONFIRMATION}} }; @@ -1127,53 +1130,22 @@ namespace tssh{ secureZeroing(genericBuffer.data(), genericBuffer.size()); } }else if(!pubKeyAuth){ - vector sign; - createAuthSign(sign, - {new VarDataString> - (sessionIdHash), - new VarDataChar(SSH_MSG_USERAUTH_REQUEST), - new VarDataString(user), - new VarDataCharArr(SSH_CONNECT_STRING), - new VarDataCharArr(SSH_PUBKEY_AUTH_REQ), - new VarDataChar(1), - new VarDataString - (get(clientPubKey)), - new VarDataString > - (get(clientPubKey)) - }); - - createSendPacket(SSH_MSG_USERAUTH_REQUEST, - {new VarDataString(user), - new VarDataCharArr(SSH_CONNECT_STRING), - new VarDataCharArr(SSH_PUBKEY_AUTH_REQ), - new VarDataChar(1), - new VarDataString - (get(clientPubKey)), - new VarDataString > - (get(clientPubKey)), - new VarDataRecursive( - { new VarDataString - (get(clientPubKey)), - new VarDataString > - (sign) - }) - }); - - pubKeyAuth = true; + TRACE(" ** SSH_MSG_USERAUTH_INFO_REQUEST: unexpected, skipping pubkey sign."); + // Send empty response to keep auth flow moving + createSendPacket(SSH_MSG_USERAUTH_INFO_RESPONSE, + {new VarDataUint32(0) + }); + pubKeyAuth = true; } break; case SSH_MSG_SERVICE_ACCEPT: - TRACE("* Received SSH_MSG_SERVICE_ACCEPT: Trying pubkey."); + TRACE("* Received SSH_MSG_SERVICE_ACCEPT: Trying none auth."); fsm.checkStatus(SSH_MSG_SERVICE_ACCEPT); + noneAuth = true; createSendPacket(SSH_MSG_USERAUTH_REQUEST, {new VarDataString(user), new VarDataCharArr(SSH_CONNECT_STRING), - new VarDataCharArr(SSH_PUBKEY_AUTH_REQ), - new VarDataChar(0), - new VarDataString( - get(clientPubKey)), - new VarDataString>( - get(clientPubKey)) + new VarDataCharArr(SSH_NONE_AUTH_REQ) }); break; case SSH_MSG_USERAUTH_SUCCESS: @@ -1189,7 +1161,20 @@ namespace tssh{ case SSH_MSG_USERAUTH_FAILURE: TRACE( "* Received SSH_MSG_USERAUTH_FAILURE."); fsm.checkStatus(SSH_MSG_USERAUTH_FAILURE); - if(!keybInter){ + if(noneAuth){ + TRACE(" ** SSH_MSG_USERAUTH_FAILURE: none auth not accepted, trying pubkey."); + noneAuth = false; + createSendPacket(SSH_MSG_USERAUTH_REQUEST, + {new VarDataString(user), + new VarDataCharArr(SSH_CONNECT_STRING), + new VarDataCharArr(SSH_PUBKEY_AUTH_REQ), + new VarDataChar(0), + new VarDataString( + get(clientPubKey)), + new VarDataString>( + get(clientPubKey)) + }); + }else if(!keybInter){ TRACE(" ** SSH_MSG_USERAUTH_FAILURE: Trying keyb-inter."); createSendPacket(SSH_MSG_USERAUTH_REQUEST, From 393aa5bc80641bb7f5116079ff106f802700b552 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 15:29:33 +0800 Subject: [PATCH 08/15] run script --- .env | 6 ++++-- README.dev.md | 38 ++++++++++++++++++++++++++++++++++++++ run.sh | 4 ++-- 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 README.dev.md diff --git a/.env b/.env index 625bf19..aaccf51 100644 --- a/.env +++ b/.env @@ -1,2 +1,4 @@ -TSSH_USERNAME="cnb-hi8-1jqqjmumq-001.5677690c-e083-4a1c-a3b7-e1157509e596-pqg" -TSSH_HOST="cnb.space" +TSSH_HOST="127.0.0.1" +TSSH_PORT="1022" +TSSH_USERNAME="admin" +TSSH_PASSWORD="123456" diff --git a/README.dev.md b/README.dev.md new file mode 100644 index 0000000..ef5e39a --- /dev/null +++ b/README.dev.md @@ -0,0 +1,38 @@ + +### 测试 + + +生成 SSH 私钥和公钥 +```bash +docker run --rm -it --entrypoint /keygen.sh linuxserver/openssh-server +``` + +启动 openssh-server + +```bash +docker run -d \ + --name=openssh-server \ + --hostname=openssh-server `#optional` \ + -e PUID=1000 \ + -e PGID=1000 \ + -e TZ=Etc/UTC \ + -e PUBLIC_KEY=yourpublickey `#optional` \ + -e PUBLIC_KEY_FILE=/path/to/file `#optional` \ + -e PUBLIC_KEY_DIR=/path/to/directory/containing/_only_/pubkeys `#optional` \ + -e PUBLIC_KEY_URL=https://github.com/username.keys `#optional` \ + -e SUDO_ACCESS=false `#optional` \ + -e PASSWORD_ACCESS=false `#optional` \ + -e USER_PASSWORD=password `#optional` \ + -e USER_PASSWORD_FILE=/path/to/file `#optional` \ + -e USER_NAME=linuxserver.io `#optional` \ + -e LOG_STDOUT= `#optional` \ + -p 2222:2222 \ + -v /path/to/openssh-server/config:/config \ + --restart unless-stopped \ + lscr.io/linuxserver/openssh-server:latest +``` + +```bash +#!/bin/bash +docker run -d --name ssh-server -p 1022:2222 -e SUDO_ACCESS=true -e USER_NAME=admin -e PASSWORD_ACCESS=true -e USER_PASSWORD=123456 lscr.io/linuxserver/openssh-server +``` diff --git a/run.sh b/run.sh index 05cfabe..668a896 100755 --- a/run.sh +++ b/run.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -eo +set -e chmod +x ./src/tssh @@ -8,4 +8,4 @@ source .env set +a # ./src/tssh -p 22 -l "$TSSH_USERNAME" -d "$TSSH_HOST" -./src/tssh -p 22 -l "$TSSH_USERNAME" "$TSSH_HOST" +./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" "$TSSH_HOST" From 3a3ed589a4a3dc0bcc7300225dbd7640b3435554 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 15:53:57 +0800 Subject: [PATCH 09/15] feat: ECDH KEX (ecdh-sha2-nistp256) --- include/Crypto.hpp | 25 +++++++ include/Tssh.hpp | 3 + src/CryptoImpl.cpp | 165 ++++++++++++++++++++++++++++++++++++++++-- src/Tssh.cpp | 173 +++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 348 insertions(+), 18 deletions(-) diff --git a/include/Crypto.hpp b/include/Crypto.hpp index aa01e0b..852af70 100644 --- a/include/Crypto.hpp +++ b/include/Crypto.hpp @@ -103,6 +103,20 @@ namespace crypto { size_t getDhHashSize(void) const noexcept override; }; + class CryptoEcdhSha2Nistp256 final : public CryptoDH{ + private: + size_t sha256Len; + + public: + CryptoEcdhSha2Nistp256(void); + ~CryptoEcdhSha2Nistp256(void) override; + void dhHash(std::vector& buff, + std::vector& hash) const noexcept override; + void dhHash(std::vector& buff, + uint8_t* hash) const noexcept override; + size_t getDhHashSize(void) const noexcept override; + }; + class CryptoHKeyAlg{ public: virtual ~CryptoHKeyAlg(void) = 0; @@ -294,6 +308,7 @@ namespace crypto { CryptoMacStC* macStC; CryptoBlkEncCtS* blkEncCtS; CryptoBlkEncStC* blkEncStC; + bool ecdhKex; const std::vector clientHKeyAlg, clientKexAlg, @@ -328,6 +343,10 @@ namespace crypto { langCtSString, langStCString; + EVP_PKEY* ecdhPkey; + EVP_PKEY* ecdhPeerKey; + std::vector ecdhQc; + void setHKeyAlg(void) anyexcept; size_t setKexAlg(void) anyexcept; void setMacAlgCtS(size_t idx) anyexcept; @@ -399,6 +418,12 @@ namespace crypto { std::vector& iv) const anyexcept; void serverKeyHash(const conceptsLib::ConstantIterable auto& in, std::vector &out) const anyexcept; + bool isEcdhKex(void) const noexcept; + void ecdhGenKey(std::vector& pubKey) anyexcept; + void ecdhSetPeerKey(const std::vector& peerKey) anyexcept; + void ecdhGetSharedKey(std::vector& shared) anyexcept; + const std::vector& + getEcdhQc(void) const noexcept; }; extern template diff --git a/include/Tssh.hpp b/include/Tssh.hpp index 27a9527..938f31a 100644 --- a/include/Tssh.hpp +++ b/include/Tssh.hpp @@ -68,6 +68,7 @@ enum STATUS { SSH_CONN_START = 0, SSH_MSG_SERVICE_REQUEST = 5, SSH_MSG_SERVICE_ACCEPT = 6, SSH_DISCONNECT_BY_APPLICATION = 11, SSH_MSG_KEXINIT = 20, SSH_MSG_NEWKEYS = 21, SSH_MSG_KEX_DH_GEX_REQUEST_OLD = 30, + SSH_MSG_KEX_ECDH_REPLY = 31, SSH_MSG_USERAUTH_REQUEST = 50, SSH_MSG_USERAUTH_FAILURE = 51, SSH_MSG_USERAUTH_SUCCESS = 52, SSH_MSG_USERAUTH_BANNER = 53, SSH_MSG_USERAUTH_INFO_REQUEST = 60, SSH_MSG_USERAUTH_INFO_RESPONSE = 61, @@ -282,6 +283,7 @@ namespace tssh{ struct termios termOld, termNew; std::string knownHosts; + bool ecdhMode; void readSsh(void) anyexcept; bool readSshEnc(int chan=-1) anyexcept; @@ -296,6 +298,7 @@ namespace tssh{ std::vector& setKexMsg(void) anyexcept; void checkServerAlgList(void) anyexcept; void checkServerDhReply(void) anyexcept; + void checkServerEcdhReply(void) anyexcept; void checkServerSignature(void) anyexcept; void createKeys(size_t keyLen) anyexcept; const std::string& getServerId(void) const noexcept; diff --git a/src/CryptoImpl.cpp b/src/CryptoImpl.cpp index 3979cf2..34a1e9e 100644 --- a/src/CryptoImpl.cpp +++ b/src/CryptoImpl.cpp @@ -94,8 +94,9 @@ namespace crypto{ Crypto::Crypto(void) : kexalg(nullptr), hKeyalg(nullptr), macCtS(nullptr), macStC(nullptr), blkEncCtS(nullptr), blkEncStC(nullptr), + ecdhKex(false), ecdhPkey(nullptr), ecdhPeerKey(nullptr), clientHKeyAlg { "rsa-sha2-256", "ssh-rsa" }, - clientKexAlg { "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, + clientKexAlg { "ecdh-sha2-nistp256", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"}, clientMacCtSAlg { "hmac-sha2-256", "hmac-sha1"}, clientMacStCAlg { "hmac-sha2-256", "hmac-sha1"}, clientBlkEncStCAlg { "aes128-ctr" }, @@ -159,6 +160,9 @@ namespace crypto{ delete blkEncCtS; delete blkEncStC; + EVP_PKEY_free(ecdhPkey); + EVP_PKEY_free(ecdhPeerKey); + EVP_cleanup(); ERR_free_strings(); ERR_remove_state(0); @@ -300,6 +304,134 @@ namespace crypto{ return hKeyalg->getDhDescr();; } + bool Crypto::isEcdhKex(void) const noexcept{ + return ecdhKex; + } + + void Crypto::ecdhGenKey(vector& pubKey) anyexcept{ + EVP_PKEY_CTX* pctx { EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr) }; + if(pctx == nullptr) + throw CryptoException(string("ecdhGenKey: EVP_PKEY_CTX_new_id failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + + if(EVP_PKEY_keygen_init(pctx) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGenKey: EVP_PKEY_keygen_init failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + if(EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGenKey: set_ec_paramgen_curve_nid failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + if(EVP_PKEY_keygen(pctx, &ecdhPkey) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGenKey: EVP_PKEY_keygen failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + EVP_PKEY_CTX_free(pctx); + + // Encode public key as uncompressed EC point (0x04 + x + y) + size_t pubLen { 0 }; + if(EVP_PKEY_get_octet_string_param(ecdhPkey, "pub", nullptr, 0, &pubLen) != 1) + throw CryptoException(string("ecdhGenKey: get pub len failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + + pubKey.resize(pubLen); + if(EVP_PKEY_get_octet_string_param(ecdhPkey, "pub", pubKey.data(), pubLen, &pubLen) != 1) + throw CryptoException(string("ecdhGenKey: get pub failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + + ecdhQc = pubKey; + TRACE("* ECDH Generated Q_C: ", &pubKey); + } + + void Crypto::ecdhSetPeerKey(const vector& peerKey) anyexcept{ + // Create EC_KEY from the peer's uncompressed point + EC_KEY* ecKey = EC_KEY_new_by_curve_name(NID_X9_62_prime256v1); + if(ecKey == nullptr) + throw CryptoException(string("ecdhSetPeerKey: EC_KEY_new_by_curve_name failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + + const unsigned char* pdata = peerKey.data(); + EC_POINT* point = EC_POINT_new(EC_KEY_get0_group(ecKey)); + if(point == nullptr){ + EC_KEY_free(ecKey); + throw CryptoException("ecdhSetPeerKey: EC_POINT_new failed."); + } + + if(EC_POINT_oct2point(EC_KEY_get0_group(ecKey), point, pdata, peerKey.size(), nullptr) != 1){ + EC_POINT_free(point); + EC_KEY_free(ecKey); + throw CryptoException(string("ecdhSetPeerKey: EC_POINT_oct2point failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + if(EC_KEY_set_public_key(ecKey, point) != 1){ + EC_POINT_free(point); + EC_KEY_free(ecKey); + throw CryptoException(string("ecdhSetPeerKey: EC_KEY_set_public_key failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + EC_POINT_free(point); + + ecdhPeerKey = EVP_PKEY_new(); + if(ecdhPeerKey == nullptr){ + EC_KEY_free(ecKey); + throw CryptoException("ecdhSetPeerKey: EVP_PKEY_new failed."); + } + + if(EVP_PKEY_assign_EC_KEY(ecdhPeerKey, ecKey) != 1){ + EC_KEY_free(ecKey); + throw CryptoException(string("ecdhSetPeerKey: EVP_PKEY_assign_EC_KEY failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + TRACE("* ECDH Set Peer Q_S: ", &peerKey); + } + + void Crypto::ecdhGetSharedKey(vector& shared) anyexcept{ + EVP_PKEY_CTX* pctx { EVP_PKEY_CTX_new(ecdhPkey, nullptr) }; + if(pctx == nullptr) + throw CryptoException(string("ecdhGetSharedKey: CTX_new failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + + if(EVP_PKEY_derive_init(pctx) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGetSharedKey: derive_init failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + if(EVP_PKEY_derive_set_peer(pctx, ecdhPeerKey) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGetSharedKey: derive_set_peer failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + size_t sharedLen { 0 }; + if(EVP_PKEY_derive(pctx, nullptr, &sharedLen) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGetSharedKey: derive (len) failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + + shared.resize(sharedLen); + if(EVP_PKEY_derive(pctx, shared.data(), &sharedLen) != 1){ + EVP_PKEY_CTX_free(pctx); + throw CryptoException(string("ecdhGetSharedKey: derive failed: ") + + ERR_error_string(ERR_get_error(), nullptr)); + } + EVP_PKEY_CTX_free(pctx); + + TRACE("* ECDH Shared Key: ", &shared); + } + + const vector& Crypto::getEcdhQc(void) const noexcept{ + return ecdhQc; + } + BIGNUM* Crypto::getE(void) const noexcept{ return hKeyalg->getE(); } @@ -317,7 +449,7 @@ namespace crypto{ size_t idx { 0 }; for(auto i {clientKexAlg.cbegin()}; i != clientKexAlg.cend(); ++i){ if(serverKexAlg->find(*i) != serverKexAlg->end()){ - found = true; + found = true; break; } idx++; @@ -327,12 +459,17 @@ namespace crypto{ switch(idx){ case 0: - kexalg = new CryptoDHG14Sha256(); - TRACE("* DH Selected: diffie-hellman-group14-sha256"); + kexalg = new CryptoEcdhSha2Nistp256(); + ecdhKex = true; + TRACE("* KEX Selected: ecdh-sha2-nistp256"); break; case 1: + kexalg = new CryptoDHG14Sha256(); + TRACE("* KEX Selected: diffie-hellman-group14-sha256"); + break; + case 2: kexalg = new CryptoDHG14Sha1(); - TRACE("* DH Selected: diffie-hellman-group14-sha1"); + TRACE("* KEX Selected: diffie-hellman-group14-sha1"); break; default: throw CryptoException("setKexAlg: Unsupported DH algorithm."); @@ -546,6 +683,24 @@ namespace crypto{ return sha256Len; } + CryptoEcdhSha2Nistp256::CryptoEcdhSha2Nistp256(void) + : sha256Len {SHA256_DIGEST_LENGTH} + {} + + CryptoEcdhSha2Nistp256::~CryptoEcdhSha2Nistp256(void){ } + + void CryptoEcdhSha2Nistp256::dhHash(vector& buff, vector& hash) const noexcept{ + static_cast(SHA256(buff.data(), buff.size(), hash.data())); + } + + void CryptoEcdhSha2Nistp256::dhHash(vector& buff, uint8_t* hash) const noexcept{ + static_cast(SHA256(buff.data(), buff.size(), hash)); + } + + size_t CryptoEcdhSha2Nistp256::getDhHashSize(void) const noexcept{ + return sha256Len; + } + CryptoKeyRsa::CryptoKeyRsa(string ids) : keyFilePrefix("id_rsa"), nullKey("FFFFFFFF"), id(ids), descr("RSA") { diff --git a/src/Tssh.cpp b/src/Tssh.cpp index fde5a9e..52157de 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -223,7 +223,7 @@ namespace tssh{ SshTransport::SshTransport(string host, string port): InetClient(host.c_str(), port.c_str()), hostname(host), - clientIdString(SSH_ID_STRING), haveKeys(false){ + clientIdString(SSH_ID_STRING), haveKeys(false), ecdhMode(false){ rndFd = open(RAND_FILE, O_RDONLY); if(rndFd == -1) @@ -761,6 +761,137 @@ namespace tssh{ haveKeys = true; } + void SshTransport::checkServerEcdhReply(void) anyexcept{ + get(dhReplyPacket) = charToUint32(buffCopy.data()); + + try{ + get(dhReplyPacket) = buffCopy.at(PADDING_LEN_OFFSET); + get(dhReplyPacket) = buffCopy.at(PACKET_TYPE_OFFSET); + }catch(...){ + throw InetException("checkServerEcdhReply: a : Invalid index."); + } + + if(get(dhReplyPacket) != SSH_MSG_KEX_ECDH_REPLY) + throw InetException(string("checkServerEcdhReply: expected KEX_ECDH_REPLY, got: ") + + to_string(get(dhReplyPacket))); + + TRACE("* Rcv SSH_MSG_KEX_ECDH_REPLY: \n ** Received bytes: " + to_string(buffCopy.size()) + + "\n ** Payload Length: " + to_string(get(dhReplyPacket) - sizeof(uint32_t)) + + "\n ** Padding Length: " + to_string(get(dhReplyPacket)) + + "\n ** Kex Packet Type: " + to_string(get(dhReplyPacket)), + &buffCopy); + + // Parse ECDH REPLY: host key blob (K_S), Q_S (EC point), signature + genericBuffer.clear(); + size_t offset { DATA_OFFSET }; + + // K_S - host certificate blob + offset += getVariableLengthRawValue(buffCopy, offset, get(dhReplyPacket)); + + // Encode host cert to B64 for known_hosts + genericBuffer.clear(); + genericBuffer.insert(genericBuffer.end(), + get(dhReplyPacket).begin(), + get(dhReplyPacket).end()); + encodeB64(genericBuffer, get(dhReplyPacket)); + + TRACE("\n ** Host Blob (Certificate) in B64: \n" + get(dhReplyPacket) + "\n"); + + // Q_S - server's EC public key point + vector qsBlob; + offset += getVariableLengthRawValue(buffCopy, offset, qsBlob); + + // Set server's public key for ECDH shared secret computation + crypto.ecdhSetPeerKey(qsBlob); + + // Compute shared secret + crypto.ecdhGetSharedKey(sharedKey); + + // Parse signature blob: string(signature_type + string(signature)) + vector sigBlob; + offset += getVariableLengthRawValue(buffCopy, offset, sigBlob); + + // Parse signature type and signature from the blob + size_t sigOff { 0 }; + sigOff += getVariableLengthRawValue(sigBlob, sigOff, get(dhReplyPacket)); + sigOff += getVariableLengthRawValue(sigBlob, sigOff, get(dhReplyPacket)); + + // Build hash buffer for signature verification (RFC 5656 section 4) + vector hashBuffer; + appendVectBuffer(hashBuffer, clientIdString.c_str(), clientIdString.size()-2, 0, clientIdString.size()-3); + appendVectBuffer(hashBuffer, serverIdString.c_str(), serverIdString.size()-2, 0, serverIdString.size()-3); + appendVectBuffer(hashBuffer, clientKexInit, 5, clientKexInit[4]); + appendVectBuffer(hashBuffer, serverKexInit, 5, serverKexInit[4]); + appendVectBuffer(hashBuffer, get(dhReplyPacket)); + appendVectBuffer(hashBuffer, crypto.getEcdhQc()); + appendVectBuffer(hashBuffer, qsBlob); + // K as mpint (add leading zero to avoid sign bit issues) + if(!sharedKey.empty() && (sharedKey[0] & 0x80)){ + genericBuffer.clear(); + genericBuffer.push_back(0); + genericBuffer.insert(genericBuffer.end(), sharedKey.begin(), sharedKey.end()); + appendVectBuffer(hashBuffer, genericBuffer); + }else{ + appendVectBuffer(hashBuffer, sharedKey); + } + + TRACE("* Hash buffer - ECDH: clientId, serverId, clientKex, serverKex, blob, Q_C, Q_S, K", + &hashBuffer); + + try{ + sessionIdHash.resize(crypto.getDhHashSize()); + }catch(...){ + throw InetException("checkServerEcdhReply: d : Data error."); + } + + crypto.dhHash(hashBuffer, sessionIdHash); + + TRACE("* ECDH Session Id dump: ", &sessionIdHash); + try{ + currentSessionHash.insert(currentSessionHash.end(), sessionIdHash.begin(), sessionIdHash.end()); + }catch(...){ + throw InetException("checkServerEcdhReply: e : Data error."); + } + + if(sessionIdHash.size() != currentHashCLen) + throw InetException("checkServerEcdhReply: Invalid Hash Size."); + + // Verify server signature - need to parse RSA key from host cert blob + // The host cert blob contains: key type string, exponent, modulus + size_t certOffset { 0 }; + string keyType; + getVariableLengthRawValue(get(dhReplyPacket), certOffset, keyType); + certOffset += sizeof(uint32_t) + keyType.size(); + + // Parse exponent and modulus from cert blob + vector expBytes, modBytes; + certOffset += getVariableLengthRawValue(get(dhReplyPacket), certOffset, expBytes); + certOffset += getVariableLengthRawValue(get(dhReplyPacket), certOffset, modBytes); + + BIGNUM* bnExp = BN_bin2bn(expBytes.data(), safeInt(expBytes.size()), nullptr); + BIGNUM* bnMod = BN_bin2bn(modBytes.data(), safeInt(modBytes.size()), nullptr); + + if(BN_num_bits(bnMod) < SSH_RSA_MIN_MODULUS_LENGTH){ + BN_free(bnExp); + BN_free(bnMod); + throw InetException("checkServerEcdhReply: Invalid Modulus size."); + } + + get(dhReplyPacket) = bnExp; + get(dhReplyPacket) = bnMod; + get(dhReplyPacket) = keyType; + + crypto.signDH(sessionIdHash, get(dhReplyPacket), + get(dhReplyPacket), get(dhReplyPacket)); + + checkServerSignature(); + + BN_free(bnExp); + BN_free(bnMod); + + haveKeys = true; + } + void SshTransport::checkServerSignature(void) anyexcept{ size_t idx { 0 }; Id line; @@ -1586,6 +1717,8 @@ namespace tssh{ readSsh(); checkServerAlgList(); + ecdhMode = crypto.isEcdhKex(); + getUserKeyFiles(); vector msg; @@ -1594,20 +1727,34 @@ namespace tssh{ }catch(...){ throw InetException("getShell: b : Data error."); } - - //DH - addHeader(SSH_MSG_KEX_DH_GEX_REQUEST_OLD, msg); - crypto.setDhKeys(genericBuffer, msg); - sendWithHeader(msg, BEGINNING_BLOCK_LEN_ALLIGN); - readSsh(); - - checkServerDhReply(); + if(ecdhMode){ + // ECDH Key Exchange (RFC 5656) + TRACE("* Starting ECDH key exchange."); + + // Generate ECDH key pair and send Q_C + vector qc; + crypto.ecdhGenKey(qc); + + addHeader(SSH_MSG_KEX_DH_GEX_REQUEST_OLD, msg); // Same msg type 30 + addVarLengthDataString(qc, msg); + sendWithHeader(msg, BEGINNING_BLOCK_LEN_ALLIGN); + + readSsh(); + checkServerEcdhReply(); + }else{ + // DH Group Key Exchange (RFC 4253) + addHeader(SSH_MSG_KEX_DH_GEX_REQUEST_OLD, msg); + crypto.setDhKeys(genericBuffer, msg); + sendWithHeader(msg, BEGINNING_BLOCK_LEN_ALLIGN); + readSsh(); + checkServerDhReply(); + } - // Read SSH_MSG_NEWKEYS packet (separate from DH Reply) + // Read SSH_MSG_NEWKEYS packet readSsh(); if(getLastPacketType() != SSH_MSG_NEWKEYS) - throw InetException(string("getShell: Invalid Packet type after DH Reply, expected: ") + + throw InetException(string("getShell: Invalid Packet type after KEX Reply, expected: ") + to_string(SSH_MSG_NEWKEYS) + " - Received: " + to_string(getLastPacketType())); @@ -1616,10 +1763,10 @@ namespace tssh{ sendWithHeader(msg, BEGINNING_BLOCK_LEN_ALLIGN); getUserPubK(); - + // Connect connectionLoop(); - + // Shell if(noTTY) shellLoop(); From 4d9e23030e0bf72c7376bcf73c18dc3786e3dd74 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 16:45:01 +0800 Subject: [PATCH 10/15] run script --- build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/build.sh b/build.sh index 18f68ed..c3167ef 100755 --- a/build.sh +++ b/build.sh @@ -8,5 +8,6 @@ make -f makefile.dist # ./configure WITH_LTO=yes +make clean make From ef9bd477fa600878e2a9585eef9c50530e9ad9e3 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 17:30:40 +0800 Subject: [PATCH 11/15] env --- .ide/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ide/Dockerfile b/.ide/Dockerfile index ad01a17..bc0f237 100644 --- a/.ide/Dockerfile +++ b/.ide/Dockerfile @@ -2,4 +2,4 @@ FROM cnbcool/default-dev-env:latest -RUN apt-get update && apt-get install -y g++ libssl-dev strace make libtool autoconf automake +RUN apt-get update && apt-get install -y g++ libssl-dev strace make libtool autoconf automake tcpdump From 6c574539961dc0752602770c9767c23ebc8b1baa Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 21:15:06 +0800 Subject: [PATCH 12/15] run script --- run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run.sh b/run.sh index 668a896..01ada5c 100755 --- a/run.sh +++ b/run.sh @@ -7,5 +7,5 @@ chmod +x ./src/tssh source .env set +a -# ./src/tssh -p 22 -l "$TSSH_USERNAME" -d "$TSSH_HOST" -./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" "$TSSH_HOST" +./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" -d "$TSSH_HOST" +# ./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" "$TSSH_HOST" From 015c5be15211305cd72feaaae28fb98a31525737 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Thu, 11 Jun 2026 23:58:09 +0800 Subject: [PATCH 13/15] fix: ssh connect --- include/Tssh.hpp | 1 + src/Tssh.cpp | 114 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 34 deletions(-) diff --git a/include/Tssh.hpp b/include/Tssh.hpp index 938f31a..eb5b242 100644 --- a/include/Tssh.hpp +++ b/include/Tssh.hpp @@ -313,6 +313,7 @@ namespace tssh{ uint8_t allign) const anyexcept; void createSendPacket(const uint8_t packetType, std::initializer_list&& list) anyexcept; + void resetSshCounters(void) noexcept; }; using StatusTree = std::map>; diff --git a/src/Tssh.cpp b/src/Tssh.cpp index 52157de..6ffbd6e 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -289,6 +289,13 @@ namespace tssh{ return clientKexInit; } + void SshTransport::resetSshCounters(void) noexcept{ + // RFC 4253 section 7.2: reset sequence numbers to zero + // before the first encrypted/authenticated message after SSH_MSG_NEWKEYS. + packetsRcvCount = numeric_limits::max(); + packetsSndCount = numeric_limits::max(); + } + const string& SshTransport::getServerId(void) const noexcept{ return serverIdString; } @@ -322,7 +329,7 @@ namespace tssh{ size_t availableBytes { 0 }, deltaBytes { 0 }; while(availableBytes < sizeof(uint32_t)){ - deltaBytes = safeSizeT(readBuffer()); + deltaBytes = safeSizeT(readBuffer(sizeof(uint32_t) - availableBytes)); if(deltaBytes > 0){ availableBytes += deltaBytes; getBufferCopy(buffCopy, true); @@ -338,7 +345,7 @@ namespace tssh{ while(requiredBytes > availableBytes){ TRACE("\n ** Read again." ); - deltaBytes = safeSizeT(readBuffer()); + deltaBytes = safeSizeT(readBuffer(requiredBytes - availableBytes)); if(deltaBytes > 0){ availableBytes += deltaBytes; getBufferCopy(buffCopy, true); @@ -434,22 +441,37 @@ namespace tssh{ plainTextLen += incomingEncLen; packetsRcvCount++; + // HMAC input: sequence_number || unencrypted_packet + // unencrypted_packet MUST include the packet_length field (RFC 4253 section 6.2) + currentHashS.resize(safeSizeT(plainTextLen + sizeof(uint32_t))); uint32ToUChars(currentHashS.data(), packetsRcvCount); try{ - currentHashS.insert(currentHashS.begin() + sizeof(uint32_t), incomingEnc.begin(), - incomingEnc.begin() + plainTextLen); + std::copy(incomingEnc.begin(), + incomingEnc.begin() + plainTextLen, + currentHashS.begin() + safePtrdiff(sizeof(uint32_t))); }catch(...){ - throw InetException("readSshEnc: d : Data error."); + throw InetException("readSshEnc: d : Data error."); } - crypto.hmacStC(currentHashS.data(), safeInt(sizeof(uint32_t) + safeULong(plainTextLen)), + crypto.hmacStC(currentHashS.data(), safeInt(plainTextLen + sizeof(uint32_t)), currentHashC.data(), ¤tHashCLen); - TRACE("* Calculating Hash - Rcv Unecrypted and Sequence: " + to_string(packetsRcvCount) + - " - Len: " + to_string(sizeof(uint32_t) + safeULong(plainTextLen)), ¤tHashS, - 0, sizeof(uint32_t), currentHashCLen + safeSizeT(plainTextLen)); + TRACE("* Calculating Hash - Rcv Unecrypted and Sequence: " + to_string(packetsRcvCount) + + " - Len: " + to_string(safeULong(plainTextLen)), ¤tHashS, + 0, sizeof(uint32_t), currentHashS.size()); TRACE("* Calculated Hash - Len: " + to_string(currentHashCLen), ¤tHashC); - + + TRACE("* Expected HMAC (from server): "); + { + size_t hmacStart = partialRead.size() - currentHashCLen; + for(size_t zi = 0; zi < currentHashCLen; zi++){ + char hexbuf[4]; + snprintf(hexbuf, sizeof(hexbuf), "%02x", partialRead[hmacStart + zi]); + cerr << hexbuf; + } + cerr << "\n"; + } + if(!equal(currentHashC.cbegin(), currentHashC.cend(), partialRead.cbegin() + safePtrdiff(partialRead.size() - currentHashCLen))) throw InetException("readSshEnc: Invalid Hash On Incoming Packet"); @@ -469,7 +491,7 @@ namespace tssh{ unsigned int hashLen { 0 }; msg[sizeof(uint32_t)] = padding; - addRandomBytes(padding, msg, msg.size()); + addRandomBytes(padding, msg, msg.size()); uint32ToUChars(msg.data(), safeUint32(msg.size() - sizeof(uint32_t))); @@ -481,15 +503,22 @@ namespace tssh{ encrTextLen += static_cast(outcomingEncLen); packetsSndCount++; + // HMAC input: sequence_number || unencrypted_packet + // unencrypted_packet MUST include the packet_length field (RFC 4253 section 6.2) + currentHashS.clear(); + currentHashS.reserve(sizeof(uint32_t) + msg.size()); + currentHashS.resize(sizeof(uint32_t)); uint32ToUChars(currentHashS.data(), packetsSndCount); + // Append the full msg including the packet_length field + currentHashS.insert(currentHashS.end(), + msg.begin(), + msg.end()); - insArrayVals(msg, 0, currentHashS, sizeof(uint32_t)); - - TRACE("\n* Snd - Sequence: " + to_string(packetsSndCount) + + TRACE("\n* Snd - Sequence: " + to_string(packetsSndCount) + "\n* Encr. payload length : " + to_string(encrTextLen) + "\n* HMAC Buffer: ", - ¤tHashS, 0, sizeof(uint32_t), msg.size()+sizeof(uint32_t)); + ¤tHashS, 0, sizeof(uint32_t), currentHashS.size()); - crypto.hmacCtS(currentHashS.data(), safeInt(msg.size() + sizeof(uint32_t)), + crypto.hmacCtS(currentHashS.data(), safeInt(currentHashS.size()), outcomingEnc.data() + encrTextLen, &hashLen); TRACE(mergeStrings({ "* HMAC Len: " , to_string(hashLen), "\n* Encr + HMAC payload: " }).c_str(), @@ -756,6 +785,11 @@ namespace tssh{ crypto.signDH(sessionIdHash, get(dhReplyPacket), get(dhReplyPacket), get(dhReplyPacket)); + // signDH() took ownership of the BNs via RSA_set0_key() and freed them + // via OPENSSL_free(serverPublicKey). Set to nullptr to prevent double-free in destructor. + get(dhReplyPacket) = nullptr; + get(dhReplyPacket) = nullptr; + checkServerSignature(); haveKeys = true; @@ -804,8 +838,23 @@ namespace tssh{ // Set server's public key for ECDH shared secret computation crypto.ecdhSetPeerKey(qsBlob); - // Compute shared secret - crypto.ecdhGetSharedKey(sharedKey); + // Compute shared secret (raw bytes) + vector sharedKeyRaw; + crypto.ecdhGetSharedKey(sharedKeyRaw); + + TRACE("* ECDH Shared Key (raw, 32 bytes): ", &sharedKeyRaw); + TRACE("* ECDH Shared Key (raw hex): "); + for(size_t zi = 0; zi < sharedKeyRaw.size(); zi++){ + char hexbuf[4]; + snprintf(hexbuf, sizeof(hexbuf), "%02x", sharedKeyRaw[zi]); + cerr << hexbuf; + } + cerr << "\n"; + + // Store as mpint in sharedKey (for key derivation in createKeys) + sharedKey.clear(); + appendVectBuffer(sharedKey, sharedKeyRaw); + TRACE("* ECDH Shared Key (mpint): ", &sharedKey); // Parse signature blob: string(signature_type + string(signature)) vector sigBlob; @@ -825,15 +874,8 @@ namespace tssh{ appendVectBuffer(hashBuffer, get(dhReplyPacket)); appendVectBuffer(hashBuffer, crypto.getEcdhQc()); appendVectBuffer(hashBuffer, qsBlob); - // K as mpint (add leading zero to avoid sign bit issues) - if(!sharedKey.empty() && (sharedKey[0] & 0x80)){ - genericBuffer.clear(); - genericBuffer.push_back(0); - genericBuffer.insert(genericBuffer.end(), sharedKey.begin(), sharedKey.end()); - appendVectBuffer(hashBuffer, genericBuffer); - }else{ - appendVectBuffer(hashBuffer, sharedKey); - } + // K as mpint (appendVectBuffer handles leading zero for mpint) + appendVectBuffer(hashBuffer, sharedKeyRaw); TRACE("* Hash buffer - ECDH: clientId, serverId, clientKex, serverKex, blob, Q_C, Q_S, K", &hashBuffer); @@ -884,10 +926,12 @@ namespace tssh{ crypto.signDH(sessionIdHash, get(dhReplyPacket), get(dhReplyPacket), get(dhReplyPacket)); - checkServerSignature(); + // signDH() took ownership of the BNs via RSA_set0_key() and freed them + // via OPENSSL_free(serverPublicKey). Set to nullptr to prevent double-free in destructor. + get(dhReplyPacket) = nullptr; + get(dhReplyPacket) = nullptr; - BN_free(bnExp); - BN_free(bnMod); + checkServerSignature(); haveKeys = true; } @@ -1048,7 +1092,6 @@ namespace tssh{ } void SshTransport::sendWithHeader(vector& buff, uint8_t allign) const anyexcept{ - const uint8_t* vectHandler { buff.data() }; uint8_t remind { safeUint8((buff.size() + 4) % allign) }, padding { safeUint8(allign - remind + 4) } ; @@ -1062,7 +1105,7 @@ namespace tssh{ size_t bufferLength { buff.size() }; uint32ToUChars(buff.data(), safeUint32(buff.size() - sizeof(uint32_t))); - writeSsh(vectHandler, bufferLength); + writeSsh(buff.data(), bufferLength); TRACE("* Send With Header - Msg type: " + to_string(buff[PACKET_TYPE_OFFSET]) + "\n ** Calculated padding: " + to_string(padding) + @@ -1759,10 +1802,13 @@ namespace tssh{ to_string(getLastPacketType())); createKeys(currentHashCLen); + + // Send NEWKEYS unencrypted - the server expects it unencrypted (no strict-kex). + // Sequence numbers continue naturally: KEXINIT(0), ECDH_REQ(1), NEWKEYS(2), SERVICE_REQUEST(3)... addHeader(SSH_MSG_NEWKEYS, msg); sendWithHeader(msg, BEGINNING_BLOCK_LEN_ALLIGN); - - getUserPubK(); + + getUserPubK(); // Connect connectionLoop(); From 83ea59ebca09e9cd3740aae60eb16254d2920c68 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Fri, 12 Jun 2026 00:01:17 +0800 Subject: [PATCH 14/15] run script --- run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run.sh b/run.sh index 01ada5c..99db3ef 100755 --- a/run.sh +++ b/run.sh @@ -7,5 +7,5 @@ chmod +x ./src/tssh source .env set +a -./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" -d "$TSSH_HOST" -# ./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" "$TSSH_HOST" +# ./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" -d "$TSSH_HOST" +./src/tssh -p $TSSH_PORT -l "$TSSH_USERNAME" "$TSSH_HOST" From cdbf8c35e0893914b4f0378ef98d18fad2b1a227 Mon Sep 17 00:00:00 2001 From: rzhangsan <4iNtJYlBfhr1QT8bkeMawE+rzhangsan@noreply.cnb.cool> Date: Fri, 12 Jun 2026 00:07:03 +0800 Subject: [PATCH 15/15] =?UTF-8?q?chore:=20=E7=A7=BB=E9=99=A4=E5=A4=9A?= =?UTF-8?q?=E4=BD=99=E7=9A=84=E8=B0=83=E8=AF=95=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Tssh.cpp | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/Tssh.cpp b/src/Tssh.cpp index 6ffbd6e..a0742fe 100644 --- a/src/Tssh.cpp +++ b/src/Tssh.cpp @@ -461,17 +461,6 @@ namespace tssh{ 0, sizeof(uint32_t), currentHashS.size()); TRACE("* Calculated Hash - Len: " + to_string(currentHashCLen), ¤tHashC); - TRACE("* Expected HMAC (from server): "); - { - size_t hmacStart = partialRead.size() - currentHashCLen; - for(size_t zi = 0; zi < currentHashCLen; zi++){ - char hexbuf[4]; - snprintf(hexbuf, sizeof(hexbuf), "%02x", partialRead[hmacStart + zi]); - cerr << hexbuf; - } - cerr << "\n"; - } - if(!equal(currentHashC.cbegin(), currentHashC.cend(), partialRead.cbegin() + safePtrdiff(partialRead.size() - currentHashCLen))) throw InetException("readSshEnc: Invalid Hash On Incoming Packet"); @@ -842,15 +831,6 @@ namespace tssh{ vector sharedKeyRaw; crypto.ecdhGetSharedKey(sharedKeyRaw); - TRACE("* ECDH Shared Key (raw, 32 bytes): ", &sharedKeyRaw); - TRACE("* ECDH Shared Key (raw hex): "); - for(size_t zi = 0; zi < sharedKeyRaw.size(); zi++){ - char hexbuf[4]; - snprintf(hexbuf, sizeof(hexbuf), "%02x", sharedKeyRaw[zi]); - cerr << hexbuf; - } - cerr << "\n"; - // Store as mpint in sharedKey (for key derivation in createKeys) sharedKey.clear(); appendVectBuffer(sharedKey, sharedKeyRaw);