From 0c2b494ac76fcb8cc942fee29df52464004ad313 Mon Sep 17 00:00:00 2001 From: thb-sb Date: Thu, 21 Mar 2024 17:13:57 +0100 Subject: [PATCH] Add support for all Diffie-Hellman Key Exchange protocols. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the SSH 2.0 protocol, there are roughly three different Diffie-Hellman key exchange protocols: - The first one, simply called Diffie-Hellman Key Exchange, defined in [RFC4253 § 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8) - The second one that use ECDH, defined in [RFC6239 § 4](https://datatracker.ietf.org/doc/html/rfc6239#section-4) - The last one, called Diffie-Hellman Group and Key Exchange, defined in [RFC4419 § 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3) The Diffie-Hellman key exhange protocol depends on the KEX algorithms that has been negociated during the _Key Exchange Init_ stage. This commit adds support for these three Diffie-Hellman key exchange protocols, by implementing a new API called `SshKEX`. To use `SshKEX`, users must have retrieved the `SshPacketKeyExchange` from the client and the server. Then, `SshKEX::init` is called to initialize the KEX stage. Later, depending on the type of the messages that come, `SshKEX::parse_ssh_packet` is called to feed the pending KEX stage. Finally, the various sub-stages specific to each DH key exchange protocols are exposed through the `SshKEX` interface. Tests have been added to ensure that these three protocols are well supported. If the feature flag `integers` is enabled, some sub-stages may expose `BigInt` instead of the integers in raw format. No copy or no memory allocation is used here. --- assets/dh_init.raw | Bin 80 -> 0 bytes assets/dh_reply.raw | Bin 296 -> 0 bytes assets/kex/dh-kex-gex/client_kex_init.raw | Bin 0 -> 1304 bytes assets/kex/dh-kex-gex/group.raw | Bin 0 -> 1048 bytes assets/kex/dh-kex-gex/init.raw | Bin 0 -> 1040 bytes assets/kex/dh-kex-gex/reply.raw | Bin 0 -> 1184 bytes assets/kex/dh-kex-gex/request.raw | Bin 0 -> 24 bytes assets/kex/dh-kex-gex/server_kex_init.raw | Bin 0 -> 528 bytes assets/kex/dh/client_kex_init.raw | Bin 0 -> 1304 bytes assets/kex/dh/init.raw | Bin 0 -> 1040 bytes assets/kex/dh/reply.raw | Bin 0 -> 1184 bytes assets/kex/dh/server_kex_init.raw | Bin 0 -> 528 bytes assets/kex/ecdh/client_kex_init.raw | Bin 0 -> 1536 bytes assets/kex/ecdh/init.raw | Bin 0 -> 48 bytes assets/kex/ecdh/reply.raw | Bin 0 -> 192 bytes assets/kex/ecdh/server_kex_init.raw | Bin 0 -> 528 bytes src/kex.rs | 703 ++++++++++++++++++++++ src/lib.rs | 10 +- src/serialize.rs | 20 +- src/ssh.rs | 146 ++--- src/tests.rs | 150 ----- tests/tests.rs | 75 +++ tests/tests_kex.rs | 212 +++++++ 23 files changed, 1058 insertions(+), 258 deletions(-) delete mode 100644 assets/dh_init.raw delete mode 100644 assets/dh_reply.raw create mode 100644 assets/kex/dh-kex-gex/client_kex_init.raw create mode 100644 assets/kex/dh-kex-gex/group.raw create mode 100644 assets/kex/dh-kex-gex/init.raw create mode 100644 assets/kex/dh-kex-gex/reply.raw create mode 100644 assets/kex/dh-kex-gex/request.raw create mode 100644 assets/kex/dh-kex-gex/server_kex_init.raw create mode 100644 assets/kex/dh/client_kex_init.raw create mode 100644 assets/kex/dh/init.raw create mode 100644 assets/kex/dh/reply.raw create mode 100644 assets/kex/dh/server_kex_init.raw create mode 100644 assets/kex/ecdh/client_kex_init.raw create mode 100644 assets/kex/ecdh/init.raw create mode 100644 assets/kex/ecdh/reply.raw create mode 100644 assets/kex/ecdh/server_kex_init.raw create mode 100644 src/kex.rs delete mode 100644 src/tests.rs create mode 100644 tests/tests.rs create mode 100644 tests/tests_kex.rs diff --git a/assets/dh_init.raw b/assets/dh_init.raw deleted file mode 100644 index 917b770091ce2bbde8963756911e212ffe449628..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80 zcmV-W0I&Z5002w{9smFUK?LVnD&e`dzI=gS$URkikH-leacu2qMQkz0^JSQ&M17-QtWyBi+2r;*tU*Q!}722NK(nC3NA|)O%mE)|_6M zaMANekDHUsqe+oZVq5R;RBu_7rP2Jt^P6(T$%Mk<;}u_K7 zH)K-x01b3xnW^LP_qA@see=I19}-^9DRyw`FV?VUm2%0xy|LZr#fxcPW;62QCbnN@ zX>)FlXStiFuB2_hgQ3a3jCZfY!E0JTLy~bj*aIk~zU9BMeFT*q&(f-MnddDuwru(F!Ea@H|D`0GDarY<9I9*o=uTBF2Dudq E001&^5C8xG diff --git a/assets/kex/dh-kex-gex/client_kex_init.raw b/assets/kex/dh-kex-gex/client_kex_init.raw new file mode 100644 index 0000000000000000000000000000000000000000..51bdc718eeb5607ff629e200b7d749988b97c5d5 GIT binary patch literal 1304 zcmds0J5s|i5EV0UgV1mWEn_)xCaK^6R5a8^*~FgMvd2;wLqoxBXgL7|HC5;sPC?Hq zA7j}Lq+G}w~vWZ)D=w& zqZC>g8Ro7xC-H7e4yDpbPI$E-(R4);x(oFB9^_eaEBapGYze!J~TP>}!D*on#%^1CFyc5<-*+5Wjpgxd23MhCU$ literal 0 HcmV?d00001 diff --git a/assets/kex/dh-kex-gex/group.raw b/assets/kex/dh-kex-gex/group.raw new file mode 100644 index 0000000000000000000000000000000000000000..6eef0bb8d174caefc3cb66c697655abe9296487e GIT binary patch literal 1048 zcmV+z1n2tz00a~W9{>OZ0RYLq&+j6UX}V~Ym3v{+I23=`=5}T#eko(urC5JgCLHL1 zS~4&dsoIx@TxTGtRr@q*fWn0N=(uJb;2 zb!{jsMI+CQz*wp}Cr@1mW}^-!oyV^^0^Adt$%&Mf8j*&zQ;QFD8h1$;6b$@g*1p%v zZj&=;FBfP&_Ulu^kuMTa1-<&MxCCp|{rX_U&J56i4MIo%_Ih%Wei+zm-`1r2^DrJV zkoEQd6OWFxtqg^&P3~=33q&<5i$cSq2gY~L${}6lIUJk*R%dC7wq0pe${*;?SJ>~! zKlN08317@dZ}YnL-CJ#k@yIsIaVu1@6~NbI507%o+~{q*=F-4Ra|W?)0Saus3#nSa zCXjKvA74W|$4v|wh?Ib)wZUjP1cQ7w$GrBN=gXGh%Aj+WB28&eurHD;Gto>qNhA42 z(F8|dNzsaTu3w}|+I(&n)3XRzA;7kQP0Lna+(hjeLdoJ%B<+?Y4Il3~7q%7~t)CXS z$O%)3EHAQhB^9v>zP^^7BN(opxE$#dQZkj1(uYoAAs}7{Dm_<884svO*PC*^G*aFR zQE6Zu+hSy{aG~5rvtxBl3QU~V_j{aQ3iru zc1K3ecJ1zF_;@RyXo{c%umgpAT4kQ9>6sTAlT+RXGhslJ;HVxGMyt)sJmI7cjtFWh zFS)pLl;HNDF`q)mwYa;S`wsd2|6nwum*XyWw)nswJ<`5`{F93QdLA8iJ4Y3=*Uz-t zoC{3>J>@d@(_R;KLSb#pKL&GZCH4&sNNrw9Hkcfd73M0}g+=poCgWb24 zlrbHzO)SES$39n|HYs}x0DFeZAn}F6seX4WkMDCGS*3iMFxU)Ui(g`KUZ^YLoLbwr zr3)%PAL43$cd4~Hww_8oMN&ymPYO9jpXjZcr?2mPvQ`guo^xI8DQe;_-``U51}^ z!9bpP4=}aB4OwO5Ud5Gr&lfBvsXjA%7Xtdwc|hgu@)chn9J4+)IW>ZsWM*2nw%4{4 zc5~n?^*f;`5lj=7cwAKtpTpydE)|wc_r6DWW~Zt%5WHt8kqiW%uTRy9TC*9LFzWM4 zXZ>=2&Uf(rRBCXpWdRsDv!)m98Y{B%L8o5Hi6VYI>pjHWnGvwOA45V^0lCvPT=`EF Smrnoy009L6000000000F-RW5X literal 0 HcmV?d00001 diff --git a/assets/kex/dh-kex-gex/init.raw b/assets/kex/dh-kex-gex/init.raw new file mode 100644 index 0000000000000000000000000000000000000000..bf9ced00df5e694f283f2b75a9c5ad1fb4a18f88 GIT binary patch literal 1040 zcmV+r1n>I*00ayMAOHXa0Ah^AVBH5@N8%wU%Eu?R_3Q9qOnOyLO;C8y&|N_i4=U0J zj^5faU3@QE^O*7)rz-(N$c3IPCSG4$@%~))ZUrAgUI5^x3UBRadly&wNZ&&p?_DEp zj7(L4XC8i~Q$$#-b*!&1`g=mVf&Bp#xOi z6*~XdU|?DckS3mP(RslEq+1?Qc0G3%#+N|zJQw1X zFJ|pp6{#jr&l^iCFtN|y33jhnu>|@#DOL~ihd1hR@ko5D`>*_KzH7{p2p0ZaN#T$A zAX@D#!Ls%KkBy}p{O8l5`0CO*AQE}H`EEAyZ>c}T3)@~gH5_2iT8Z?^^E-|X9d}Q! zO7_AEv)v($Z-b>1$5p{0aVxR9Wbv|S#{Cm<*41>?4ZWjb^Z5Ri5EGAcjD2L#DC~a& z?XdNB!E{71X$0f!q9oz8++rb==B*{5NFyugy4%ZsAJOdnk0rt6r05rs`B>zj4jK33 z>76-ZKffYV6BrI{1OqQ9bF255NY*+MEKyHm>fxNlU=q9@MB*Ma(s+HnC+Dv->a4xm z0Fc%q&*tVZ{)xXKjU?!pY?s**yBkpPLg;VHFpjTu*{cB8u+fcp|;kNwKLIAvg6*ZSPqIcxex5i%EFkEtNK3ZQ&1z}u6!77co zlN)te2Q&d*!JUa?lxCNK$#eelHP2?3V~A^a6$<<8QS}CaHa~V0EMLJGxIV&>F=j-2 z|DMdV96=&5{>rk(x}w?I#_b2{N(y>YtWogXX1DkPzFYPPlByrI&O(A4fBK)L_-1Sl zEJ2sl))OW4h^HnfLgf_Ik^so$`HDoSv5G*a_3n^AYrOve3p^Q29=XdL(M&uF3D{WI zZJP@OJBKYHe18H!DY4l-6(vu=W=)&?Xcrm`)C$UFzlq>7{ouo(ew zJ#zn5`un55RwK$Pe)jm-@@bJAVioKyPkmqU(wIm^EfD{dP*X(pvV7Xb#+-V9jK`cZ KI{*Lx00019*ZwL1 literal 0 HcmV?d00001 diff --git a/assets/kex/dh-kex-gex/reply.raw b/assets/kex/dh-kex-gex/reply.raw new file mode 100644 index 0000000000000000000000000000000000000000..3692fad40f9b50410418a74be18b7398e93af358 GIT binary patch literal 1184 zcmV;R1Yi3A00f)|ApigXGXMYp3v+X5EoEdfH8n9g0000WGqkFP+QYhHuEj104%QTO z?m$6rc0nN)Ie*xJ8LF_3000C50GY^*y$9QNCuY};!6b&;;v3oVa*c;b&3Vg(A0-Wp zLSV=M!sToqtr*^pzr0p0s510S$Z+eD<*({Nn-*FW;5uy2i)dN21dXS%hEWtwlfYR5 z3?uh*g0_YMNb3PK`(B}8h1f0N)#s_9E+CV6JbB%Bcx(zye2SRQERy<2WP&OFcbhS$ z@CgQ)Ua5+SVYl7v=U^x34}d7GmPGs!T|Xi>6zXGCcuF|02+OJ%Kx&w&4-tmIwq{OO zHwJid55_LfvSQ0XA2ktODKNoj6;T_Db4W8(pC^p9T-?n|^NiAP{Svb;8b&>}65anU zKy~@7esd=48@Sw>vM|_ab;q)>>=*$8_*l}2F`^I3#^(dFXc8z^nDuzl(Sq?AG8LO> zgdqd)lE$Qky|jUVE9%D4czqk+24XMBn41dO?MVL9oxvDbJc%|m!-|Uh=wmd|uM`Ha ztxq6b#0kx0bB1Km?VPcRZ8UFg;uV|F%GG8@N~DjB9#d8*dM$t$?Gk!_nKY{DSbFq< z@s5-^-oCp}#CSvNSqPwI6i*asntPHicKj!|0Z=@X(=>9CUVOt{BI`t?LF#FS-RiVB zAxUfPe3Z+*s}cI-pIg#yD-_Eooj-z!QRW_h$T6E2N2LRZSZ`$07CNI(CfF{1cp&Gu zOcyRG>1|6c>Od~W^Soy#U-X-luLz9(`oZvypt@=UYr5el$UgFcBQ@bjWw4AiiN*8` z6J!+k`Y_}$U|!KG|7poVo{h*_KsEr_`$+S%zFC8s2&DXzwlh}4gf+@tE+i;ZBO#OU zUHO~E-HxHLy^~T5k=}u&{My0?f3Zp+!!`9Y?Di*RYIBdK_m4?5gQ}#ZJ>evU!f_n2 zSlTKQruZP5a>m&m7XabHCRUa?o%tVaH(R}7^_iEZ8-7@;Ma}sz{M%x*V4aAdbeu3k zNW|DqkkC8+{b<3wX+;r?m_eC`AkIu1NlFk1!oR8@_Ahr)%AtF5qH7_FnjgKWK^XO7 zKEcY&T8N5lP?5akckMU~LLtp>JkoUSS|VOnl{TV$m%g0FS;*zu&6uJ4rQ|k=jd!?ujAUP{Dc>IYnVh z)Y=vYNvQhx)$yY+joX9gkDW7oFvU}DR%L;a{QKr`Wm8AXmVRlY%EVg70qk#;XR9R6 zRH}>YIR+f{bB45+=a^oZCFucWul^&njlxhIj|8J{&PUtWjKUsIcX|_J>8qwc5DuODA zn_gmJ5&!@IQvd(}3v+X5EoEdfH8n9g0000$cjpT)n=@G|Dzp(=zGw(N-3MD*l06Hi yjT&R7$1#i?L=N!Nf|^A>IY?eFAAjMM8Dp57I_J*$)jn?~pbvcl0000000010Eh|O< literal 0 HcmV?d00001 diff --git a/assets/kex/dh-kex-gex/request.raw b/assets/kex/dh-kex-gex/request.raw new file mode 100644 index 0000000000000000000000000000000000000000..cea9cc041d36d8fcee98dccd06d9157865f66bf5 GIT binary patch literal 24 YcmZQzU=U$bVqoB4U|>*yVj#r;00+7Ng#Z8m literal 0 HcmV?d00001 diff --git a/assets/kex/dh-kex-gex/server_kex_init.raw b/assets/kex/dh-kex-gex/server_kex_init.raw new file mode 100644 index 0000000000000000000000000000000000000000..1e8448e9017eacd7ed1c6c1ccf9b1423043fcaf6 GIT binary patch literal 528 zcma)3u};G<5Vblmu~ec0Oe<4YKxJ|vQJVzpEUEoA;D zn|(TC(z}>U-wE>CHIx9u2?S{DL9CTz{u>2Vz;T%jRjU#*5Eiz)VvhCsPGm16=GuILs*d5xNe(J z=Kq3N|&+FAmH9{F7rZWN<|kUPYD^l;u2)QU6;*4$WkMR?Q^IU zf%4789I{yHg1fU}#ECZ@MszvrCaNX;Di1k>6_%9|%&5C#&FFQxC#Wl$7KSObFf>eU zZ&u>nmMluG)4X=QLD4uHiHLmsxdTO>Jg|x5T1S1U823QvlA7fllh`gdmUk!;79wq) zGmHlO2mRhwsV`tQ1*vU?8{Bwn79w+CGr`6|f7OgdUzN2gI~i6;l>`g4+SBiydAn-I bFvx#v>@3SONgtE=&dS9AX#3|X658Jv!xgX; literal 0 HcmV?d00001 diff --git a/assets/kex/dh/init.raw b/assets/kex/dh/init.raw new file mode 100644 index 0000000000000000000000000000000000000000..db52ecf30f217339d39f7157618e1e19616de9f3 GIT binary patch literal 1040 zcmV+r1n>I*00ayL9smFY0RV^PGOvdJH%*k*s~WANiig+>R_weSzCxWcand#H8loCN zSKzga-w66*V#;~pvvK!S>z8i0uZo?0OSH^rkb<(N4KimhW;RQmcjI}*5qERJqLNYN ztJR9@BeF&)y5P*BvgIL&?f9V46u`zW&1qN%UO{+cYkhJR(jssmgr6>0bnfrG#~)uJ zaYD!%jXfz1M~!6^1ql56MHBWk z^MYW>yeT5VD_-~=Uf0;UQJCypd`tE*3LdWsDYoaK(pLpWf4?m}oVZ6Ez&Pbl{Hrk} zY+FR9V70~F=Cn})kXXdZozNJRYd9H7I&N)5P#QqF7;+`t?07OZZG(@*Cs0x+;!D`J z&c``=%1gAj!pH57GfNL_$UtdRH=N($DuXFsuzS?Y2xW8~AE<6+$+7ISiFAt?{YJnj zLAnaHOucnKOPCyoF#C^UXB5te^+ZrksPf|R#hw)(*q5QOZ>O1&gu}vs7e7lOc)wW@ z_>8x2%Y3kAB3Jn>h#*D{jvI%`1q2lDu5a^%_vmun1R1{yGkek}>{DX`nQY9?58YJm z$og-91<2QC?8(`VwzkPsDNBY4&LaI@d%q4IUJF3f4?DhZ?Cq-#!0TK4hxM^qXC#|V zJ2pMI`umYn3UuRf5nA(yB}KAG8h*4)EN68qE0>wx@8z9s6g=#VzhARr04mXS4(^3@ zti@iv9Cyx76m8K%vk&`z{wG59Fgg@(Hn3S!OS2nkxV3comE1i=+VS5i;1fGlX*1UW&{Vry@&h#dQe8Jg zin=j(hnie#VG+9DABK2xeqhN#np`w|m4&ax6U8l-oCs1w8`1xQ4=Cc4fP`M9ki;qW?g{MVL&2y4%-_cB9 zzGV2dwsUluH6uA5*Rv$LY$4@+b!BIwlHl`zwMw*|3O1cyZoDFxIS9-v-wqZp12@_R z)+((QcL$jbbQq&+xy}$+#XMS=CxYE#5dYWm;aL1FAj&AN&DrV!^*RwZG&YZKHhg0w zC+3g&4@O?crJw3Y6?0`X(Dr<%md7;{d0&}%6i*OVGXMYp3v+X5EoEdfH8n9g0000WGqkFP+QYhHuEj104%QTO z?m$6rc0nN)Ie*xJ8LF_3000C4FzAx8@D?>*fnK}D09!taQtc+NfyifZC)&>{zdsyf z1y=YbBhFyU0l7wKm`Fc#dsQ)2EIP`3eG|%j!u5pmhhx>GEz`KCZF0G?7xN4#*o_%L z;fbW6Dx|%ZGa(Cl1UzOTPD909<9o5|Pp=bbWP(aivQx)Y+WHp;0q#1iE{%WMPut{} zVoa-!to71Jr-MZ&Q(#NFZF;)E;(E`-|0OJze)EQnOM6cZ0tWwqFp$R+6f59PMs;!zC4$Dv$ofS|@4MFmD0I>F84mgd+J*0`{t<-N1 zvV<~ko7Z`9#)*b3`=XW9PRvQXVMLQf`WOuUnh#juK$KRDk`(QatAwP@yCE-!C9x8P z1e3I_pU#sh0|$5U6+fJT#KGiVS?N`{HFi<@INYdW^b@Vj)HQjCVlHdpS&ZFO207!;k!ixla`7U7l9jsbm(0sTR3) zmEA7o0000}0000Bb8~1dWn?lnH8D8=002OKaUd|Fu5K`!?N?{djylxpe znN(byp_gBju9KRak^xqzo0nN!QUFqqCS+`3f-YogWT=ypnUT9KTQn3tXkvl^QsLo<-9si6^8BMdF@%9=n;2Kv;os5lYK(*>CY=YV_w^qekG z0OV|#8o2L3a;Yic00J7KkeFI*Xk?+ATvDV1Vp(xQ3!d8s;8z-V#EFG$S;#Z7X4E>Is%37{Mb007ypqhSC5 literal 0 HcmV?d00001 diff --git a/assets/kex/ecdh/client_kex_init.raw b/assets/kex/ecdh/client_kex_init.raw new file mode 100644 index 0000000000000000000000000000000000000000..359daab43e713d160aa772311bb22810168c35e2 GIT binary patch literal 1536 zcmds1F;2rU6b%FF1(+C_82nKjn$T_)w?JY^T{nrEIFapCfsNC^#z_zpS771F40#*>R4j5?YqURX{7x_vsi^ z9QxFo#TP8k2rK21w^h#%wvkDiObtDk5#ekWn>HSZMCrotJJ985*oAzLl36mJ zCmdp)rdcQ;l6hGGuUH(4h}RvlRUe54YuWwP(cbE?>C7t)iJ0fW$bwg(v`m-=220o7 zi~~b^MXSJPt4J7^8aZ^6W8A5*Zrp|d2N5xv!-yDIKT$2kW&4&6jHV$@I%~ROEt_d;GClxflR#@2?`kzP4MCHb3i@-dQKN8 z0CF}=4cvDixzrSJ009kANK7p@G_uf5E-BIhF)fW?OrSX+CXs5SGjbD?A+7;A0Ko&g l4QK=bY0 IResult<&[u8], BigInt> { + nom::combinator::map_parser(parse_string, crate::mpint::parse_ssh_mpint)(i) +} + +#[cfg(not(feature = "integers"))] +fn parse_mpint(i: &[u8]) -> IResult<&[u8], &[u8]> { + parse_string(i) +} + +/// SSH Diffie-Hellman Key Exchange Init. +/// +/// The message code is `SSH_MSG_KEXDH_INIT`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). +#[derive(Debug, PartialEq)] +pub struct SshPacketDHKEXInit<'a> { + /// The public key. + #[cfg(feature = "integers")] + pub e: BigInt, + + /// The public key. + #[cfg(not(feature = "integers"))] + pub e: &'a [u8], + + #[cfg(feature = "integers")] + phantom: std::marker::PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From for SshPacketDHKEXInit<'_> { + fn from(e: BigInt) -> Self { + Self { + e, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b> From<&'b [u8]> for SshPacketDHKEXInit<'b> +where + 'b: 'a, +{ + fn from(e: &'b [u8]) -> Self { + Self { e } + } +} + +impl<'a> SshPacketDHKEXInit<'a> { + /// Parses a SSH Diffie-Hellman Key Exchange Init. + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_mpint, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Key Exchange Reply. +/// +/// The message code is `SSH_MSG_KEXDH_REPLY`, defined in [RFC4253 section 8](https://datatracker.ietf.org/doc/html/rfc4253#section-8). +#[derive(Debug, PartialEq)] +pub struct SshPacketDHKEXReply<'a> { + /// The server public host key and certificate. + pub pubkey_and_cert: &'a [u8], + + /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. + #[cfg(feature = "integers")] + pub f: BigInt, + + /// The `f` value corresponding to `g^y mod p` where `g` is the group and `y` a random number. + #[cfg(not(feature = "integers"))] + pub f: &'a [u8], + + /// The signature. + pub signature: &'a [u8], +} + +#[cfg(feature = "integers")] +impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +/// An ECDSA signature. +/// +/// ECDSA signatures are defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). +#[derive(Debug, PartialEq)] +pub struct ECDSASignature<'a> { + /// Identifier. + pub identifier: &'a str, + + /// Blob. + pub blob: &'a [u8], +} + +impl<'a> SshPacketDHKEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) + } + + /// Parses the ECDSA signature. + /// + /// ECDSA signatures are Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). + pub fn get_ecdsa_signature(&self) -> Result, SshKEXError<'a>> { + let (_, (identifier, blob)) = pair(parse_string, parse_string)(self.signature)?; + + let identifier = std::str::from_utf8(identifier)?; + Ok(ECDSASignature { identifier, blob }) + } +} + +/// The key exchange protocol using Diffie Hellman Key Exchange, defined in RFC4253. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXDiffieHellman<'a> { + /// The init message, i.e. `SSH_MSG_KEXDH_INIT`. + pub init: Option>, + + /// The reply message, i.e. `SSH_MSG_KEXDH_REPLY`. + pub reply: Option>, +} + +/// SSH Elliptic Curve Diffie-Hellman Key Exchange Init. +/// +/// The message is `SSH_MSG_KEXECDH_INIT`, defined in [RFC6239 section 4.1](https://datatracker.ietf.org/doc/html/rfc6239#section-4.1). +#[derive(Debug, PartialEq)] +pub struct SshPacketECDHKEXInit<'a> { + /// The client's ephemeral contribution to theECDH exchange, encoded as an octet string. + pub q_c: &'a [u8], +} + +impl<'a, 'b> From<&'b [u8]> for SshPacketECDHKEXInit<'a> +where + 'b: 'a, +{ + fn from(q_c: &'b [u8]) -> Self { + Self { q_c } + } +} + +impl<'a> SshPacketECDHKEXInit<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_string, Self::from)(i) + } +} + +/// SSH Elliptic Curve Diffie-Hellman Key Exchange Reply. +/// +/// The message is `SSH_MSG_KEXECDH_REPLY`, defined in [RFC6239 section 4.2](https://datatracker.ietf.org/doc/html/rfc6239#section-4.2). +#[derive(Debug, PartialEq)] +pub struct SshPacketECDHKEXReply<'a> { + /// A string encoding an X.509v3 certificate containing the server's ECDSA public host key. + pub pubkey_and_cert: &'a [u8], + + /// The server's ephemeral contribution to the ECDH exchange, encoded as an octet string. + pub q_s: &'a [u8], + + /// The server's signature of the newly established exchange hash value. + pub signature: &'a [u8], +} + +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketECDHKEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, q_s, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + q_s, + signature, + } + } +} + +impl<'a> SshPacketECDHKEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map( + tuple((parse_string, parse_string, parse_string)), + Self::from, + )(i) + } +} + +/// The key exchange protocol using Elliptic Curve Diffie Hellman Key Exchange, defined in RFC6239. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXECDiffieHellman<'a> { + /// The init message, i.e. `SSH_MSG_KEXECDH_INIT`. + pub init: Option>, + + /// The reply message, i.e. `SSH_MSG_KEXECDH_REPLY`. + pub reply: Option>, +} + +/// SSH Diffie-Hellman Group and Key Exchange Request. +/// +/// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXRequest { + /// Minimal size in bits of an acceptable group. + pub min: u32, + + /// Preferred size in bits of the group the server will send. + pub n: u32, + + /// Maximal size in bits of an acceptable group. + pub max: u32, +} + +impl From<(u32, u32, u32)> for SshPacketDhKEXGEXRequest { + fn from((min, n, max): (u32, u32, u32)) -> Self { + Self { min, n, max } + } +} + +impl SshPacketDhKEXGEXRequest { + pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { + map(tuple((be_u32, be_u32, be_u32)), Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Request (old). +/// +/// The message code is `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXRequestOld { + /// Preferred size in bits of the group the server will send. + pub n: u32, +} + +impl From for SshPacketDhKEXGEXRequestOld { + fn from(n: u32) -> Self { + Self { n } + } +} + +impl SshPacketDhKEXGEXRequestOld { + pub fn parse(i: &[u8]) -> IResult<&[u8], Self> { + map(be_u32, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Group. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_GROUP`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXGroup<'a> { + /// The safe prime. + #[cfg(feature = "integers")] + pub p: BigInt, + + /// The safe prime. + #[cfg(not(feature = "integers"))] + pub p: &'a [u8], + + /// The generator for the subgroup in the Galois Field GF(p). + #[cfg(feature = "integers")] + pub g: BigInt, + + /// The generator for the subgroup in the Galois Field GF(p). + #[cfg(not(feature = "integers"))] + pub g: &'a [u8], + + #[cfg(feature = "integers")] + phantom: PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From<(BigInt, BigInt)> for SshPacketDhKEXGEXGroup<'_> { + fn from((p, g): (BigInt, BigInt)) -> Self { + Self { + p, + g, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c> From<(&'b [u8], &'c [u8])> for SshPacketDhKEXGEXGroup<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((p, g): (&'b [u8], &'c [u8])) -> Self { + Self { p, g } + } +} + +impl<'a> SshPacketDhKEXGEXGroup<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(pair(parse_mpint, parse_mpint), Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Init. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_INIT`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXInit<'a> { + /// The public key. + #[cfg(feature = "integers")] + pub e: BigInt, + + /// The public key. + #[cfg(not(feature = "integers"))] + pub e: &'a [u8], + + #[cfg(feature = "integers")] + phantom: PhantomData<&'a [u8]>, +} + +#[cfg(feature = "integers")] +impl From for SshPacketDhKEXGEXInit<'_> { + fn from(e: BigInt) -> Self { + Self { + e, + phantom: PhantomData, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b> From<&'b [u8]> for SshPacketDhKEXGEXInit<'a> +where + 'b: 'a, +{ + fn from(e: &'b [u8]) -> Self { + Self { e } + } +} + +/// Parses a SSH Diffie-Hellman Group and Key Exchange init. +impl<'a> SshPacketDhKEXGEXInit<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(parse_mpint, Self::from)(i) + } +} + +/// SSH Diffie-Hellman Group and Key Exchange Reply. +/// +/// The message code is `SSH_MSG_KEX_DH_GEX_REPLY`, defined in [RFC4419 section 5](https://datatracker.ietf.org/doc/html/rfc4419#section-5). +/// +/// The message is defined in [RFC4419 section 3](https://datatracker.ietf.org/doc/html/rfc4419#section-3). +#[derive(Debug, PartialEq)] +pub struct SshPacketDhKEXGEXReply<'a> { + /// Server public host key and certificate. + pub pubkey_and_cert: &'a [u8], + + /// f. + #[cfg(feature = "integers")] + pub f: BigInt, + + /// f. + #[cfg(not(feature = "integers"))] + pub f: &'a [u8], + + /// Signature. + pub signature: &'a [u8], +} + +#[cfg(feature = "integers")] +impl<'a, 'b, 'c> From<(&'b [u8], BigInt, &'c [u8])> for SshPacketDhKEXGEXReply<'a> +where + 'b: 'a, + 'c: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], BigInt, &'c [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +#[cfg(not(feature = "integers"))] +impl<'a, 'b, 'c, 'd> From<(&'b [u8], &'c [u8], &'d [u8])> for SshPacketDhKEXGEXReply<'a> +where + 'b: 'a, + 'c: 'a, + 'd: 'a, +{ + fn from((pubkey_and_cert, f, signature): (&'b [u8], &'c [u8], &'d [u8])) -> Self { + Self { + pubkey_and_cert, + f, + signature, + } + } +} + +impl<'a> SshPacketDhKEXGEXReply<'a> { + pub fn parse(i: &'a [u8]) -> IResult<&'a [u8], Self> { + map(tuple((parse_string, parse_mpint, parse_string)), Self::from)(i) + } +} + +/// The key exchange protocol using Diffie Hellman Group and Key, defined in RFC4419. +#[derive(Debug, Default, PartialEq)] +pub struct SshKEXDiffieHellmanKEXGEX<'a> { + /// The request message, i.e. `SSH_MSG_KEY_DH_GEX_REQUEST`. + pub request: Option, + + /// The request message (old variant), i.e. `SSH_MSG_KEY_DH_GEX_REQUEST_OLD`. + pub request_old: Option, + + /// The group message, i.e. `SSH_MSG_KEX_DH_GEX_GROUP`. + pub group: Option>, + + /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_INIT`. + pub init: Option>, + + /// The init message, i.e. `SSH_MSG_KEX_DH_GEX_REPLY`. + pub reply: Option>, +} + +/// An error occurring in the KEX parser. +#[derive(Debug)] +pub enum SshKEXError<'a> { + /// nom error. + Nom(nom::Err>), + + /// Could not negociate a KEX algorithm. + NegociationFailed, + + /// Unknown KEX protocol. + UnknownProtocol, + + /// Duplicated message. + DuplicatedMessage, + + /// Unexpected message. + UnexpectedMessage, + + /// Invalid UTF-8 string. + InvalidUtf8(std::str::Utf8Error), + + /// Other error. + Other(String), +} + +impl std::fmt::Display for SshKEXError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +impl<'a> From>> for SshKEXError<'a> { + fn from(e: nom::Err>) -> Self { + Self::Nom(e) + } +} + +impl From for SshKEXError<'_> { + fn from(e: String) -> Self { + Self::Other(e) + } +} + +impl From<&str> for SshKEXError<'_> { + fn from(e: &str) -> Self { + Self::Other(e.to_string()) + } +} + +impl From for SshKEXError<'_> { + fn from(e: std::str::Utf8Error) -> Self { + Self::InvalidUtf8(e) + } +} + +impl std::error::Error for SshKEXError<'_> {} + +macro_rules! parse_match_and_assign { + ($variant:ident, $field:ident, $struct:ident, $payload:ident) => { + if $variant.$field.is_some() { + Err(SshKEXError::DuplicatedMessage) + } else { + $variant.$field = Some(all_consuming($struct::parse)($payload)?.1); + Ok(()) + } + }; +} + +/// Negociates the KEX algorithm. +pub fn ssh_kex_negociate_algorithm<'a, 'b, 'c, S1, S2>( + client_kex_algs: impl IntoIterator, + server_kex_algs: impl IntoIterator, +) -> Option<&'a str> +where + 'b: 'a, + 'c: 'a, + S1: AsRef + 'b + ?Sized, + S2: AsRef + 'c + ?Sized, +{ + let server_algs = server_kex_algs + .into_iter() + .map(|s| s.as_ref()) + .collect::>(); + client_kex_algs + .into_iter() + .find(|&item| server_algs.contains(&item.as_ref())) + .map(|s| s.as_ref()) +} + +/// The key exchange protocol. +#[derive(Debug, PartialEq)] +pub enum SshKEX<'a> { + /// Diffie Hellman Key Exchange, defined in RFC4253. + DiffieHellman(SshKEXDiffieHellman<'a>), + + /// Elliptic Curve Diffie Hellman, defined in RFC6239. + ECDiffieHellman(SshKEXECDiffieHellman<'a>), + + /// Diffie Hellman Group and Key, defined in RFC4419. + DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX<'a>), +} + +impl<'a> SshKEX<'a> { + /// Initializes a [`SshKEX`] using the kex algorithms sent during the kex exchange + /// init stage. + /// The returned string is the negociated KEX algorithm. + pub fn init<'b, 'c>( + client_kex_init: &'b SshPacketKeyExchange<'b>, + server_kex_init: &'c SshPacketKeyExchange<'c>, + ) -> Result<(Self, &'a str), SshKEXError<'a>> + where + 'b: 'a, + 'c: 'a, + { + let client_kex_list = client_kex_init.get_kex_algs()?; + let server_kex_list = server_kex_init.get_kex_algs()?; + let negociated_alg = ssh_kex_negociate_algorithm(client_kex_list, server_kex_list) + .ok_or(SshKEXError::NegociationFailed)?; + match negociated_alg { + "diffie-hellman-group1-sha1" + | "diffie-hellman-group14-sha1" + | "diffie-hellman-group14-sha256" + | "diffie-hellman-group16-sha512" + | "diffie-hellman-group18-sha512" => { + Ok(Self::DiffieHellman(SshKEXDiffieHellman::default())) + } + "curve25519-sha256" + | "curve25519-sha256@libssh.org" + | "curve448-sha512" + | "ecdh-sha2-nistp256" + | "ecdh-sha2-nistp384" + | "ecdh-sha2-nistp521" => Ok(Self::ECDiffieHellman(SshKEXECDiffieHellman::default())), + "diffie-hellman-group-exchange-sha1" | "diffie-hellman-group-exchange-sha256" => Ok( + Self::DiffieHellmanKEXGEX(SshKEXDiffieHellmanKEXGEX::default()), + ), + _ => Err(SshKEXError::UnknownProtocol), + } + .map(|kex| (kex, negociated_alg)) + } + + /// Parses a new message according to the selected KEX method. + /// If the parsed message is not related to the KEX protocol, SshKEXError::UnexpectedMessage + /// is returned. + pub fn parse_ssh_packet<'c>( + &mut self, + unparsed_ssh_packet: &'c SshPacketUnparsed<'c>, + ) -> Result<(), SshKEXError<'a>> + where + 'c: 'a, + { + let payload = unparsed_ssh_packet.payload; + match self { + Self::DiffieHellman(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEXDH_INIT => { + parse_match_and_assign!(dh, init, SshPacketDHKEXInit, payload) + } + SSH_MSG_KEXDH_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketDHKEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + Self::ECDiffieHellman(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEXECDH_INIT => { + parse_match_and_assign!(dh, init, SshPacketECDHKEXInit, payload) + } + SSH_MSG_KEXECDH_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketECDHKEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + Self::DiffieHellmanKEXGEX(dh) => match unparsed_ssh_packet.message_code { + SSH_MSG_KEX_DH_GEX_REQUEST => { + parse_match_and_assign!(dh, request, SshPacketDhKEXGEXRequest, payload) + } + SSH_MSG_KEX_DH_GEX_REQUEST_OLD => { + parse_match_and_assign!(dh, request_old, SshPacketDhKEXGEXRequestOld, payload) + } + SSH_MSG_KEX_DH_GEX_GROUP => { + parse_match_and_assign!(dh, group, SshPacketDhKEXGEXGroup, payload) + } + SSH_MSG_KEX_DH_GEX_INIT => { + parse_match_and_assign!(dh, init, SshPacketDhKEXGEXInit, payload) + } + SSH_MSG_KEX_DH_GEX_REPLY => { + parse_match_and_assign!(dh, reply, SshPacketDhKEXGEXReply, payload) + } + _ => Err(SshKEXError::UnexpectedMessage), + }, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index f098267..52d5dbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,13 +3,19 @@ //! The code is available on [GitHub](https://github.com/rusticata/ssh-parser) //! and is part of the [Rusticata](https://github.com/rusticata) project. +pub mod kex; #[cfg(feature = "integers")] pub mod mpint; #[cfg(feature = "serialize")] /// SSH packet crafting functions pub mod serialize; mod ssh; -#[cfg(test)] -mod tests; +pub use kex::{ + ssh_kex_negociate_algorithm, ECDSASignature, SshKEX, SshKEXDiffieHellman, + SshKEXDiffieHellmanKEXGEX, SshKEXECDiffieHellman, SshKEXError, SshPacketDHKEXInit, + SshPacketDHKEXReply, SshPacketDhKEXGEXGroup, SshPacketDhKEXGEXInit, SshPacketDhKEXGEXReply, + SshPacketDhKEXGEXRequest, SshPacketDhKEXGEXRequestOld, SshPacketECDHKEXInit, + SshPacketECDHKEXReply, +}; pub use ssh::*; diff --git a/src/serialize.rs b/src/serialize.rs index b7b6455..bd29784 100644 --- a/src/serialize.rs +++ b/src/serialize.rs @@ -1,6 +1,4 @@ -use crate::ssh::{ - SshPacket, SshPacketDebug, SshPacketDhReply, SshPacketDisconnect, SshPacketKeyExchange, -}; +use super::{SshPacket, SshPacketDebug, SshPacketDisconnect, SshPacketKeyExchange}; use cookie_factory::gen::{set_be_u32, set_be_u8}; use cookie_factory::*; use std::iter::repeat; @@ -31,16 +29,6 @@ fn gen_packet_key_exchange<'a>( ) } -fn gen_packet_dh_reply<'a>( - x: (&'a mut [u8], usize), - p: &SshPacketDhReply, -) -> Result<(&'a mut [u8], usize), GenError> { - do_gen!( - x, - gen_string(p.pubkey_and_cert) >> gen_string(p.f) >> gen_string(p.signature) - ) -} - fn gen_packet_disconnect<'a>( x: (&'a mut [u8], usize), p: &SshPacketDisconnect, @@ -73,8 +61,7 @@ fn packet_payload_type(p: &SshPacket) -> u8 { SshPacket::ServiceAccept(_) => 6, SshPacket::KeyExchange(_) => 20, SshPacket::NewKeys => 21, - SshPacket::DiffieHellmanInit(_) => 30, - SshPacket::DiffieHellmanReply(_) => 31, + SshPacket::DiffieHellmanKEX(ref p) => p.0.message_code, } } @@ -91,8 +78,7 @@ fn gen_packet_payload<'a>( SshPacket::ServiceAccept(p) => gen_string(x, p), SshPacket::KeyExchange(ref p) => gen_packet_key_exchange(x, p), SshPacket::NewKeys => Ok(x), - SshPacket::DiffieHellmanInit(ref p) => gen_string(x, p.e), - SshPacket::DiffieHellmanReply(ref p) => gen_packet_dh_reply(x, p), + SshPacket::DiffieHellmanKEX(ref p) => gen_string(x, p.0.payload), } } diff --git a/src/ssh.rs b/src/ssh.rs index 3eae177..2a68020 100644 --- a/src/ssh.rs +++ b/src/ssh.rs @@ -5,11 +5,11 @@ use nom::bytes::streaming::{is_not, tag, take, take_until}; use nom::character::streaming::{crlf, line_ending, not_line_ending}; -use nom::combinator::{complete, map, map_parser, map_res, opt}; +use nom::combinator::{complete, map, map_res, opt}; use nom::error::{make_error, Error, ErrorKind}; use nom::multi::{length_data, many_till, separated_list1}; use nom::number::streaming::{be_u32, be_u8}; -use nom::sequence::{delimited, pair, terminated}; +use nom::sequence::{delimited, terminated}; use nom::{Err, IResult}; use rusticata_macros::newtype_enum; use std::str; @@ -61,7 +61,7 @@ pub fn parse_ssh_identification(i: &[u8]) -> IResult<&[u8], (Vec<&[u8]>, SshVers } #[inline] -fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> { +pub(super) fn parse_string(i: &[u8]) -> IResult<&[u8], &[u8]> { length_data(be_u32)(i) } @@ -189,73 +189,6 @@ impl<'a> SshPacketKeyExchange<'a> { } } -/// SSH Key Exchange Client Packet -/// -/// Defined in [RFC4253 section 8](https://tools.ietf.org/html/rfc4253#section-8) and [errata](https://www.rfc-editor.org/errata_search.php?rfc=4253). -/// -/// The single field e is left unparsed because its representation depends on -/// the negotiated key exchange algorithm: -/// -/// - with a diffie hellman exchange on multiplicative group of integers modulo -/// p, such as defined in [RFC4253](https://tools.ietf.org/html/rfc4253), the -/// field is a multiple precision integer (defined in [RFC4251 section 5](https://tools.ietf.org/html/rfc4251#section-5)). -/// - with a DH on elliptic curves, such as defined in [RFC6239](https://tools.ietf.org/html/rfc6239), the field is an octet string. -/// -/// TODO: add accessors for the different representations -#[derive(Debug, PartialEq)] -pub struct SshPacketDhInit<'a> { - pub e: &'a [u8], -} - -fn parse_packet_dh_init(i: &[u8]) -> IResult<&[u8], SshPacket> { - map(parse_string, |e| { - SshPacket::DiffieHellmanInit(SshPacketDhInit { e }) - })(i) -} - -/// SSH Key Exchange Server Packet -/// -/// Defined in [RFC4253 section 8](https://tools.ietf.org/html/rfc4253#section-8) and [errata](https://www.rfc-editor.org/errata_search.php?rfc=4253). -/// -/// Like the client packet, the fields depend on the algorithm negotiated during -/// the previous packet exchange. -#[derive(Debug, PartialEq)] -pub struct SshPacketDhReply<'a> { - pub pubkey_and_cert: &'a [u8], - pub f: &'a [u8], - pub signature: &'a [u8], -} - -fn parse_packet_dh_reply(i: &[u8]) -> IResult<&[u8], SshPacket> { - let (i, pubkey_and_cert) = parse_string(i)?; - let (i, f) = parse_string(i)?; - let (i, signature) = parse_string(i)?; - let reply = SshPacketDhReply { - pubkey_and_cert, - f, - signature, - }; - Ok((i, SshPacket::DiffieHellmanReply(reply))) -} - -impl<'a> SshPacketDhReply<'a> { - /// Parse the ECDSA server signature. - /// - /// Defined in [RFC5656 Section 3.1.2](https://tools.ietf.org/html/rfc5656#section-3.1.2). - #[allow(clippy::type_complexity)] - pub fn get_ecdsa_signature(&self) -> Result<(&str, Vec), nom::Err>> { - let (i, identifier) = map_res(parse_string, str::from_utf8)(self.signature)?; - let (_, blob) = map_parser(parse_string, pair(parse_string, parse_string))(i)?; - - let mut rs = Vec::new(); - - rs.extend_from_slice(blob.0); - rs.extend_from_slice(blob.1); - - Ok((identifier, rs)) - } -} - /// SSH Disconnection Message /// /// Defined in [RFC4253 Section 11.1](https://tools.ietf.org/html/rfc4253#section-11.1). @@ -345,6 +278,11 @@ impl<'a> SshPacketDebug<'a> { } } +/// A SSH message that may belong to the KEX stage. +/// use [`super::SshKEX`] to parse this message. +#[derive(Debug, PartialEq)] +pub struct MaybeDiffieHellmanKEX<'a>(pub SshPacketUnparsed<'a>); + /// SSH Packet Enumeration #[derive(Debug, PartialEq)] pub enum SshPacket<'a> { @@ -356,8 +294,7 @@ pub enum SshPacket<'a> { ServiceAccept(&'a [u8]), KeyExchange(SshPacketKeyExchange<'a>), NewKeys, - DiffieHellmanInit(SshPacketDhInit<'a>), - DiffieHellmanReply(SshPacketDhReply<'a>), + DiffieHellmanKEX(MaybeDiffieHellmanKEX<'a>), } /// Parse a plaintext SSH packet with its padding. @@ -365,29 +302,60 @@ pub enum SshPacket<'a> { /// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and /// message codes are defined in [RFC4253 Section 12](https://tools.ietf.org/html/rfc4253#section-12). pub fn parse_ssh_packet(i: &[u8]) -> IResult<&[u8], (SshPacket, &[u8])> { + let (i, unparsed_ssh_packet) = parse_ssh_packet_with_message_code(i)?; + let padding = unparsed_ssh_packet.padding; + let d = unparsed_ssh_packet.payload; + let (_, msg) = match unparsed_ssh_packet.message_code { + 1 => parse_packet_disconnect(d), + 2 => map(parse_string, SshPacket::Ignore)(d), + 3 => map(be_u32, SshPacket::Unimplemented)(d), + 4 => parse_packet_debug(d), + 5 => map(parse_string, SshPacket::ServiceRequest)(d), + 6 => map(parse_string, SshPacket::ServiceAccept)(d), + 20 => parse_packet_key_exchange(d), + 21 => Ok((d, SshPacket::NewKeys)), + 30..=34 => Ok(( + i, + SshPacket::DiffieHellmanKEX(MaybeDiffieHellmanKEX(unparsed_ssh_packet)), + )), + _ => Err(Err::Error(make_error(d, ErrorKind::Switch))), + }?; + Ok((i, (msg, padding))) +} + +/// A plaintext SSH packet in raw format, with the message code. +#[derive(Debug, PartialEq)] +pub struct SshPacketUnparsed<'a> { + /// The payload, **without** the message code byte. + pub payload: &'a [u8], + + /// The padding. + pub padding: &'a [u8], + + /// The message code. + pub message_code: u8, +} + +/// Parse a plaintext SSH packet header with its message code. +/// +/// Packet structure is defined in [RFC4253 Section 6](https://tools.ietf.org/html/rfc4253#section-6) and +pub fn parse_ssh_packet_with_message_code(i: &[u8]) -> IResult<&[u8], SshPacketUnparsed> { let (i, packet_length) = be_u32(i)?; let (i, padding_length) = be_u8(i)?; if padding_length as u32 + 1 > packet_length { return Err(Err::Error(make_error(i, ErrorKind::LengthValue))); } - let (i, payload) = map_parser(take(packet_length - padding_length as u32 - 1), |d| { - let (d, msg_type) = be_u8(d)?; - match msg_type { - 1 => parse_packet_disconnect(d), - 2 => map(parse_string, SshPacket::Ignore)(d), - 3 => map(be_u32, SshPacket::Unimplemented)(d), - 4 => parse_packet_debug(d), - 5 => map(parse_string, SshPacket::ServiceRequest)(d), - 6 => map(parse_string, SshPacket::ServiceAccept)(d), - 20 => parse_packet_key_exchange(d), - 21 => Ok((d, SshPacket::NewKeys)), - 30 => parse_packet_dh_init(d), - 31 => parse_packet_dh_reply(d), - _ => Err(Err::Error(make_error(d, ErrorKind::Switch))), - } - })(i)?; + let (i, payload) = take(packet_length - padding_length as u32 - 1)(i)?; + let (payload_without_message_code, message_code) = be_u8(payload)?; let (i, padding) = take(padding_length)(i)?; - Ok((i, (payload, padding))) + Ok(( + i, + SshPacketUnparsed { + payload: payload_without_message_code, + padding, + message_code, + }, + )) } #[cfg(test)] diff --git a/src/tests.rs b/src/tests.rs deleted file mode 100644 index c1e2f8e..0000000 --- a/src/tests.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Public API tests -use nom::error::{make_error, ErrorKind}; -use nom::Err; - -use super::ssh::*; - -static CLIENT_KEY_EXCHANGE: &[u8] = include_bytes!("../assets/client_init.raw"); -static CLIENT_DH_INIT: &[u8] = include_bytes!("../assets/dh_init.raw"); -static SERVER_DH_REPLY: &[u8] = include_bytes!("../assets/dh_reply.raw"); -static SERVER_NEW_KEYS: &[u8] = include_bytes!("../assets/new_keys.raw"); -static SERVER_COMPAT: &[u8] = include_bytes!("../assets/server_compat.raw"); - -#[test] -fn test_identification() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"2.0", - software: b"OpenSSH_7.3", - comments: None, - }; - - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(&CLIENT_KEY_EXCHANGE[..21]); - assert_eq!(res, expected); -} - -#[test] -fn test_compatibility() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"1.99", - software: b"OpenSSH_3.1p1", - comments: None, - }; - - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(&SERVER_COMPAT[..23]); - assert_eq!(res, expected); -} - -#[test] -fn test_version_with_comments() { - let empty: Vec<&[u8]> = vec![]; - let version = SshVersion { - proto: b"2.0", - software: b"OpenSSH_7.3", - comments: Some(b"toto"), - }; - let expected = Ok((b"" as &[u8], (empty, version))); - let res = parse_ssh_identification(b"SSH-2.0-OpenSSH_7.3 toto\r\n"); - assert_eq!(res, expected); -} - -#[test] -fn test_client_key_exchange() { - let cookie = [ - 0xca, 0x98, 0x42, 0x14, 0xd6, 0xa5, 0xa7, 0xfd, 0x6c, 0xe8, 0xd4, 0x7c, 0x0b, 0xc0, 0x96, - 0xcc, - ]; - let key_exchange = SshPacket::KeyExchange(SshPacketKeyExchange { - cookie: &cookie, - kex_algs: b"curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", - server_host_key_algs: b"ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa", - encr_algs_client_to_server: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", - encr_algs_server_to_client: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", - mac_algs_client_to_server: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", - mac_algs_server_to_client: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", - comp_algs_client_to_server: b"none,zlib@openssh.com,zlib", - comp_algs_server_to_client: b"none,zlib@openssh.com,zlib", - langs_client_to_server: b"", - langs_server_to_client: b"", - first_kex_packet_follows: false, - }); - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - - let expected = Ok((b"" as &[u8], (key_exchange, padding))); - let res = parse_ssh_packet(&CLIENT_KEY_EXCHANGE[21..]); - assert_eq!(res, expected); -} - -#[test] -fn test_dh_init() { - let e = [ - 0x04, 0xe7, 0x59, 0x2a, 0xe1, 0xb9, 0xb6, 0xbe, 0x7c, 0x81, 0x5f, 0xc8, 0x3d, 0x55, 0x7b, - 0x8f, 0xc7, 0x09, 0x1d, 0x71, 0x6c, 0xed, 0x68, 0x45, 0x6c, 0x31, 0xc7, 0xf3, 0x65, 0x98, - 0xa5, 0x44, 0x7d, 0xa4, 0x28, 0xdd, 0xe7, 0x3a, 0xd9, 0xa1, 0x0e, 0x4b, 0x75, 0x3a, 0xde, - 0x33, 0x99, 0x6e, 0x41, 0x7d, 0xea, 0x88, 0xe9, 0x90, 0xe3, 0x5a, 0x27, 0xf8, 0x38, 0x09, - 0x01, 0x66, 0x46, 0xd4, 0xdc, - ]; - let dh = SshPacket::DiffieHellmanInit(SshPacketDhInit { e: &e }); - let padding: &[u8] = &[0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (dh, padding))); - let res = parse_ssh_packet(CLIENT_DH_INIT); - assert_eq!(res, expected); -} - -#[test] -fn test_dh_reply() { - let pubkey = [ - 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, - 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x08, 0x6e, 0x69, 0x73, - 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x41, 0x04, 0x55, 0xa1, 0xb5, 0x65, 0xde, - 0xf5, 0x6a, 0xac, 0xcb, 0xa9, 0x60, 0xd1, 0x49, 0xf8, 0x8c, 0x46, 0x42, 0x1c, 0xe2, 0x92, - 0x59, 0xe4, 0x5d, 0x85, 0xdf, 0xb9, 0x27, 0x84, 0xa2, 0x6a, 0x28, 0x83, 0xe8, 0x49, 0xf6, - 0x23, 0x78, 0xc9, 0x60, 0x71, 0x73, 0xc7, 0x78, 0xf5, 0x83, 0x85, 0xdd, 0xcf, 0x74, 0x63, - 0x0e, 0xbd, 0xcf, 0x78, 0x33, 0xeb, 0x5e, 0xfa, 0xfe, 0x2f, 0xd8, 0x1c, 0x65, 0xbc, - ]; - let f = [ - 0x04, 0x99, 0x2c, 0x48, 0xfd, 0xeb, 0x2d, 0x58, 0xdf, 0x37, 0xfd, 0x74, 0xf0, 0x60, 0xe9, - 0x9c, 0x73, 0x40, 0x42, 0x8f, 0x73, 0x28, 0x3f, 0x05, 0x1a, 0x44, 0x6b, 0xdb, 0xb1, 0x87, - 0x4c, 0xe8, 0xe8, 0x96, 0x4a, 0x36, 0x98, 0x6e, 0x5e, 0x91, 0x87, 0xd3, 0x04, 0x86, 0x43, - 0x83, 0x5f, 0x04, 0xdd, 0x6e, 0x27, 0x22, 0x2b, 0x3f, 0xb8, 0x00, 0x82, 0x3f, 0x76, 0x0d, - 0xbd, 0x40, 0xc1, 0xd6, 0x2a, - ]; - let signature = [ - 0x00, 0x00, 0x00, 0x13, 0x65, 0x63, 0x64, 0x73, 0x61, 0x2d, 0x73, 0x68, 0x61, 0x32, 0x2d, - 0x6e, 0x69, 0x73, 0x74, 0x70, 0x32, 0x35, 0x36, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00, 0x00, - 0x20, 0x0b, 0xca, 0x56, 0x33, 0xaf, 0xe5, 0xd6, 0x72, 0xaf, 0x3f, 0x8c, 0x1a, 0x8c, 0x28, - 0x50, 0x6d, 0x3f, 0x5a, 0xa4, 0x55, 0xba, 0x80, 0x4d, 0x98, 0x16, 0x56, 0x9b, 0x6b, 0x1f, - 0x79, 0x21, 0xc8, 0x00, 0x00, 0x00, 0x20, 0x0c, 0xa5, 0x7a, 0xce, 0x69, 0xcf, 0x38, 0x28, - 0xb4, 0xb4, 0xf8, 0xf0, 0x4e, 0xa9, 0x67, 0x8f, 0xd2, 0x62, 0x3c, 0x94, 0x63, 0x6f, 0x5d, - 0x08, 0x25, 0xad, 0xfc, 0x2d, 0x95, 0x25, 0x73, 0xbc, - ]; - let dh = SshPacket::DiffieHellmanReply(SshPacketDhReply { - pubkey_and_cert: &pubkey, - f: &f, - signature: &signature, - }); - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (dh, padding))); - let res = parse_ssh_packet(SERVER_DH_REPLY); - assert_eq!(res, expected); -} - -#[test] -fn test_new_keys() { - let keys = SshPacket::NewKeys; - let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - let expected = Ok((b"" as &[u8], (keys, padding))); - let res = parse_ssh_packet(SERVER_NEW_KEYS); - assert_eq!(res, expected); -} - -#[test] -fn test_invalid_packet0() { - let data = b"\x00\x00\x00\x00\x00\x00\x00\x00"; - let expected = Err(Err::Error(make_error(&data[5..], ErrorKind::LengthValue))); - let res = parse_ssh_packet(data); - assert_eq!(res, expected); -} diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..28e3087 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,75 @@ +// Public API tests +extern crate ssh_parser; + +use ssh_parser::*; + +static CLIENT_KEY_EXCHANGE: &[u8] = include_bytes!("../assets/client_init.raw"); +static SERVER_COMPAT: &[u8] = include_bytes!("../assets/server_compat.raw"); + +#[test] +fn test_identification() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"2.0", + software: b"OpenSSH_7.3", + comments: None, + }; + + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(&CLIENT_KEY_EXCHANGE[..21]); + assert_eq!(res, expected); +} + +#[test] +fn test_compatibility() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"1.99", + software: b"OpenSSH_3.1p1", + comments: None, + }; + + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(&SERVER_COMPAT[..23]); + assert_eq!(res, expected); +} + +#[test] +fn test_version_with_comments() { + let empty: Vec<&[u8]> = vec![]; + let version = SshVersion { + proto: b"2.0", + software: b"OpenSSH_7.3", + comments: Some(b"toto"), + }; + let expected = Ok((b"" as &[u8], (empty, version))); + let res = parse_ssh_identification(b"SSH-2.0-OpenSSH_7.3 toto\r\n"); + assert_eq!(res, expected); +} + +#[test] +fn test_client_key_exchange() { + let cookie = [ + 0xca, 0x98, 0x42, 0x14, 0xd6, 0xa5, 0xa7, 0xfd, 0x6c, 0xe8, 0xd4, 0x7c, 0x0b, 0xc0, 0x96, + 0xcc, + ]; + let key_exchange = SshPacket::KeyExchange(SshPacketKeyExchange { + cookie: &cookie, + kex_algs: b"curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha1,diffie-hellman-group14-sha256,diffie-hellman-group14-sha1,ext-info-c", + server_host_key_algs: b"ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,ssh-rsa-cert-v01@openssh.com,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,ssh-ed25519,rsa-sha2-512,rsa-sha2-256,ssh-rsa", + encr_algs_client_to_server: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", + encr_algs_server_to_client: b"chacha20-poly1305@openssh.com,aes128-ctr,aes192-ctr,aes256-ctr,aes128-gcm@openssh.com,aes256-gcm@openssh.com,aes128-cbc,aes192-cbc,aes256-cbc,3des-cbc", + mac_algs_client_to_server: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", + mac_algs_server_to_client: b"umac-64-etm@openssh.com,umac-128-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,hmac-sha1-etm@openssh.com,umac-64@openssh.com,umac-128@openssh.com,hmac-sha2-256,hmac-sha2-512,hmac-sha1", + comp_algs_client_to_server: b"none,zlib@openssh.com,zlib", + comp_algs_server_to_client: b"none,zlib@openssh.com,zlib", + langs_client_to_server: b"", + langs_server_to_client: b"", + first_kex_packet_follows: false, + }); + let padding: &[u8] = &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + let expected = Ok((b"" as &[u8], (key_exchange, padding))); + let res = parse_ssh_packet(&CLIENT_KEY_EXCHANGE[21..]); + assert_eq!(res, expected); +} diff --git a/tests/tests_kex.rs b/tests/tests_kex.rs new file mode 100644 index 0000000..43f77e3 --- /dev/null +++ b/tests/tests_kex.rs @@ -0,0 +1,212 @@ +// Public API tests for KEX. +extern crate ssh_parser; + +use ssh_parser::*; + +fn load_client_server_key_exchange_init( + client: &'static [u8], + server: &'static [u8], +) -> (SshPacketKeyExchange<'static>, SshPacketKeyExchange<'static>) { + let client = parse_ssh_packet(client).unwrap().1 .0; + let server = parse_ssh_packet(server).unwrap().1 .0; + assert!(matches!( + (&client, &server), + (SshPacket::KeyExchange(_), SshPacket::KeyExchange(_)) + )); + match (client, server) { + (SshPacket::KeyExchange(client), SshPacket::KeyExchange(server)) => (client, server), + _ => unreachable!(), + } +} + +fn load_kex_packet(packet: &[u8]) -> SshPacketUnparsed<'_> { + let kex_packet = parse_ssh_packet(packet).unwrap().1 .0; + assert!(matches!(&kex_packet, SshPacket::DiffieHellmanKEX(_))); + match kex_packet { + SshPacket::DiffieHellmanKEX(kex) => kex.0, + _ => unreachable!(), + } +} + +mod ecdh { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/ecdh/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/ecdh/server_kex_init.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/ecdh/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/ecdh/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "curve25519-sha256"); + assert!(matches!(kex, SshKEX::ECDiffieHellman(_))); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::ECDiffieHellman(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + } +} + +mod dh { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = include_bytes!("../assets/kex/dh/server_kex_init.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/dh/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/dh/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "diffie-hellman-group18-sha512"); + assert!(matches!(kex, SshKEX::DiffieHellman(_))); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::DiffieHellman(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + + let ecdsa_signature = kex.reply.as_ref().unwrap().get_ecdsa_signature().unwrap(); + assert_eq!(ecdsa_signature.identifier, "ssh-ed25519"); + } +} + +mod dh_kex_gex { + use super::*; + + static CLIENT_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/dh-kex-gex/client_kex_init.raw"); + static SERVER_KEY_EXCHANGE_INIT: &[u8] = + include_bytes!("../assets/kex/dh-kex-gex/server_kex_init.raw"); + static REQUEST: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/request.raw"); + static GROUP: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/group.raw"); + static INIT: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/init.raw"); + static REPLY: &[u8] = include_bytes!("../assets/kex/dh-kex-gex/reply.raw"); + + #[test] + fn test_kex() { + let (client_kex, server_kex) = load_client_server_key_exchange_init( + CLIENT_KEY_EXCHANGE_INIT, + SERVER_KEY_EXCHANGE_INIT, + ); + let (mut kex, negociated_alg) = SshKEX::init(&client_kex, &server_kex).unwrap(); + assert_eq!(negociated_alg, "diffie-hellman-group-exchange-sha256"); + assert!(matches!(kex, SshKEX::DiffieHellmanKEXGEX(_))); + + let request_packet = load_kex_packet(REQUEST); + assert!(matches!(kex.parse_ssh_packet(&request_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&request_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let group_packet = load_kex_packet(GROUP); + assert!(matches!(kex.parse_ssh_packet(&group_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&group_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let init_packet = load_kex_packet(INIT); + assert!(matches!(kex.parse_ssh_packet(&init_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&init_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let reply_packet = load_kex_packet(REPLY); + assert!(matches!(kex.parse_ssh_packet(&reply_packet), Ok(()))); + assert!(matches!( + kex.parse_ssh_packet(&reply_packet), + Err(SshKEXError::DuplicatedMessage) + )); + + let kex = match kex { + SshKEX::DiffieHellmanKEXGEX(kex) => kex, + _ => unreachable!(), + }; + + assert!(kex.request.is_some()); + assert!(kex.group.is_some()); + assert!(kex.init.is_some()); + assert!(kex.reply.is_some()); + } +} + +mod kex_algorithm_negociation { + use super::ssh_kex_negociate_algorithm; + + #[test] + fn test_negociation() { + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["a", "b", "c"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "a", "c"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["b", "d", "c"]), + Some("b") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "c", "e"]), + Some("c") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["c", "b", "a"]), + Some("a") + ); + assert_eq!( + ssh_kex_negociate_algorithm(["a", "b", "c"], ["d", "e", "f"]), + None + ); + } +}