Skip to content

Commit 9c86cb8

Browse files
committed
Merge branch 'main' of github.com:warp-tech/russh
2 parents a391c45 + 9e1068b commit 9c86cb8

6 files changed

Lines changed: 230 additions & 6 deletions

File tree

.all-contributorsrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,15 @@
814814
"contributions": [
815815
"code"
816816
]
817+
},
818+
{
819+
"login": "tluyben",
820+
"name": "tluyben",
821+
"avatar_url": "https://avatars.githubusercontent.com/u/623448?v=4",
822+
"profile": "https://github.com/tluyben",
823+
"contributions": [
824+
"code"
825+
]
817826
}
818827
],
819828
"contributorsPerLine": 7,

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Russh
22

33
[![Rust](https://github.com/warp-tech/russh/actions/workflows/rust.yml/badge.svg)](https://github.com/warp-tech/russh/actions/workflows/rust.yml) <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
4-
[![All Contributors](https://img.shields.io/badge/all_contributors-90-orange.svg?style=flat-square)](#contributors-)
4+
[![All Contributors](https://img.shields.io/badge/all_contributors-91-orange.svg?style=flat-square)](#contributors-)
55
<!-- ALL-CONTRIBUTORS-BADGE:END -->
66

77
Low-level Tokio SSH2 client and server implementation.
@@ -257,6 +257,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
257257
<td align="center" valign="top" width="14.28%"><a href="http://l9o.dev"><img src="https://avatars.githubusercontent.com/u/112069?v=4?s=100" width="100px;" alt="Luiz Ribeiro"/><br /><sub><b>Luiz Ribeiro</b></sub></a><br /><a href="https://github.com/Eugeny/russh/commits?author=luizribeiro" title="Code">💻</a></td>
258258
<td align="center" valign="top" width="14.28%"><a href="http://ctx.st"><img src="https://avatars.githubusercontent.com/u/259330610?v=4?s=100" width="100px;" alt="biao29"/><br /><sub><b>biao29</b></sub></a><br /><a href="https://github.com/Eugeny/russh/commits?author=biao29" title="Code">💻</a></td>
259259
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gvz"><img src="https://avatars.githubusercontent.com/u/3962183?v=4?s=100" width="100px;" alt="Georg von Zengen"/><br /><sub><b>Georg von Zengen</b></sub></a><br /><a href="https://github.com/Eugeny/russh/commits?author=gvz" title="Code">💻</a></td>
260+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tluyben"><img src="https://avatars.githubusercontent.com/u/623448?v=4?s=100" width="100px;" alt="tluyben"/><br /><sub><b>tluyben</b></sub></a><br /><a href="https://github.com/Eugeny/russh/commits?author=tluyben" title="Code">💻</a></td>
260261
</tr>
261262
</tbody>
262263
</table>

russh/src/client/encrypted.rs

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ impl Session {
255255
key: key.clone(),
256256
hash_alg,
257257
},
258+
None,
258259
&mut self.common.buffer,
259260
)?;
260261
let len = self.common.buffer.len();
@@ -284,6 +285,7 @@ impl Session {
284285
let i = enc.client_make_to_sign(
285286
&self.common.auth_user,
286287
&PublicKeyOrCertificate::Certificate(cert.clone()),
288+
hash_alg,
287289
&mut self.common.buffer,
288290
)?;
289291
let len = self.common.buffer.len();
@@ -1216,6 +1218,100 @@ mod tests {
12161218
assert_eq!(Vec::<u8>::decode(&mut mic).unwrap(), b"mic".to_vec());
12171219
ensure_end(&mic).unwrap();
12181220
}
1221+
1222+
1223+
fn rsa_user_certificate() -> ssh_key::Certificate {
1224+
let subject = ssh_key::PrivateKey::random(
1225+
&mut rand::rng(),
1226+
ssh_key::Algorithm::Rsa { hash: None },
1227+
)
1228+
.unwrap();
1229+
let ca = ssh_key::PrivateKey::random(&mut rand::rng(), ssh_key::Algorithm::Ed25519)
1230+
.unwrap();
1231+
let mut builder = ssh_key::certificate::Builder::new_with_random_nonce(
1232+
&mut rand::rng(),
1233+
subject.public_key(),
1234+
0,
1235+
u64::MAX,
1236+
)
1237+
.unwrap();
1238+
builder.key_id("rsa-user-cert").unwrap();
1239+
builder
1240+
.cert_type(ssh_key::certificate::CertType::User)
1241+
.unwrap();
1242+
builder.valid_principal("alice").unwrap();
1243+
builder.sign(&ca).unwrap()
1244+
}
1245+
1246+
fn decode_userauth_certificate_algorithm(
1247+
mut request: &[u8],
1248+
expected_has_signature: bool,
1249+
) -> String {
1250+
assert_eq!(u8::decode(&mut request).unwrap(), msg::USERAUTH_REQUEST);
1251+
assert_eq!(String::decode(&mut request).unwrap(), "alice");
1252+
assert_eq!(String::decode(&mut request).unwrap(), "ssh-connection");
1253+
assert_eq!(String::decode(&mut request).unwrap(), "publickey");
1254+
assert_eq!(
1255+
u8::decode(&mut request).unwrap(),
1256+
u8::from(expected_has_signature)
1257+
);
1258+
let algorithm = String::decode(&mut request).unwrap();
1259+
let _certificate = Vec::<u8>::decode(&mut request).unwrap();
1260+
ensure_end(&request).unwrap();
1261+
algorithm
1262+
}
1263+
1264+
#[test]
1265+
fn rsa_future_certificate_hash_is_encoded_in_probe_and_signed_requests() {
1266+
let certificate = rsa_user_certificate();
1267+
for (hash_alg, expected) in [
1268+
(
1269+
Some(ssh_key::HashAlg::Sha512),
1270+
"rsa-sha2-512-cert-v01@openssh.com",
1271+
),
1272+
(
1273+
Some(ssh_key::HashAlg::Sha256),
1274+
"rsa-sha2-256-cert-v01@openssh.com",
1275+
),
1276+
(None, "ssh-rsa-cert-v01@openssh.com"),
1277+
] {
1278+
let mut encrypted = test_encrypted();
1279+
encrypted
1280+
.write_auth_request(
1281+
"alice",
1282+
&auth::Method::FutureCertificate {
1283+
cert: certificate.clone(),
1284+
hash_alg,
1285+
},
1286+
)
1287+
.unwrap();
1288+
let probe_payloads = payloads(&encrypted.write);
1289+
assert_eq!(probe_payloads.len(), 1);
1290+
assert_eq!(
1291+
decode_userauth_certificate_algorithm(probe_payloads[0], false),
1292+
expected
1293+
);
1294+
1295+
let mut signed = Vec::new();
1296+
encrypted
1297+
.client_make_to_sign(
1298+
"alice",
1299+
&PublicKeyOrCertificate::Certificate(certificate.clone()),
1300+
hash_alg,
1301+
&mut signed,
1302+
)
1303+
.unwrap();
1304+
let mut signed_request = signed.as_slice();
1305+
assert_eq!(
1306+
Vec::<u8>::decode(&mut signed_request).unwrap(),
1307+
b"session-id"
1308+
);
1309+
assert_eq!(
1310+
decode_userauth_certificate_algorithm(signed_request, true),
1311+
expected
1312+
);
1313+
}
1314+
}
12191315
}
12201316

12211317
impl Encrypted {
@@ -1281,13 +1377,14 @@ impl Encrypted {
12811377
key.to_bytes()?.as_slice().encode(&mut self.write)?;
12821378
true
12831379
}
1284-
auth::Method::FutureCertificate { ref cert, .. } => {
1380+
auth::Method::FutureCertificate { ref cert, hash_alg } => {
12851381
user.as_bytes().encode(&mut self.write)?;
12861382
"ssh-connection".encode(&mut self.write)?;
12871383
"publickey".encode(&mut self.write)?;
12881384
self.write.push(0); // This is a probe
12891385

12901386
cert.algorithm()
1387+
.with_hash_alg(hash_alg)
12911388
.to_certificate_type()
12921389
.encode(&mut self.write)?;
12931390
cert.to_bytes()?.as_slice().encode(&mut self.write)?;
@@ -1370,6 +1467,7 @@ impl Encrypted {
13701467
&mut self,
13711468
user: &str,
13721469
key: &PublicKeyOrCertificate,
1470+
certificate_hash_alg: Option<ssh_key::HashAlg>,
13731471
buffer: &mut Vec<u8>,
13741472
) -> Result<usize, crate::Error> {
13751473
buffer.clear();
@@ -1384,7 +1482,10 @@ impl Encrypted {
13841482

13851483
match key {
13861484
PublicKeyOrCertificate::Certificate(cert) => {
1387-
cert.algorithm().to_certificate_type().encode(buffer)?;
1485+
cert.algorithm()
1486+
.with_hash_alg(certificate_hash_alg)
1487+
.to_certificate_type()
1488+
.encode(buffer)?;
13881489
cert.to_bytes()?.encode(buffer)?;
13891490
}
13901491
PublicKeyOrCertificate::PublicKey { key, hash_alg } => {
@@ -1404,7 +1505,7 @@ impl Encrypted {
14041505
match method {
14051506
auth::Method::PublicKey { key } => {
14061507
let i0 =
1407-
self.client_make_to_sign(user, &PublicKeyOrCertificate::from(key), buffer)?;
1508+
self.client_make_to_sign(user, &PublicKeyOrCertificate::from(key), None, buffer)?;
14081509

14091510
// Extend with self-signature.
14101511
sign_with_hash_alg(key, buffer)?.encode(&mut *buffer)?;
@@ -1418,6 +1519,7 @@ impl Encrypted {
14181519
let i0 = self.client_make_to_sign(
14191520
user,
14201521
&PublicKeyOrCertificate::Certificate(cert.clone()),
1522+
None,
14211523
buffer,
14221524
)?;
14231525

russh/src/client/session.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,16 @@ impl Session {
126126
pix_width.encode(&mut enc.write)?;
127127
pix_height.encode(&mut enc.write)?;
128128

129-
((1 + 5 * terminal_modes.len()) as u32).encode(&mut enc.write)?;
129+
// TTY_OP_END entries are skipped below and the single
130+
// terminator is written afterwards, so the length must
131+
// count only the modes that are actually encoded --
132+
// otherwise the declared string length overruns the
133+
// bytes written and the rest of the packet is garbage.
134+
let encoded_modes = terminal_modes
135+
.iter()
136+
.filter(|&&(code, _)| code != Pty::TTY_OP_END)
137+
.count();
138+
((1 + 5 * encoded_modes) as u32).encode(&mut enc.write)?;
130139
for &(code, value) in terminal_modes {
131140
if code == Pty::TTY_OP_END {
132141
continue;

russh/src/server/encrypted.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1394,6 +1394,11 @@ impl Session {
13941394
map_err!(ensure_end(r))?;
13951395

13961396
if let Some(chan) = self.channels.get(&channel_num) {
1397+
// Only the first `i` entries were decoded from the
1398+
// request; the rest of `modes` is padding and must
1399+
// not be passed on as if the client had sent it.
1400+
#[allow(clippy::indexing_slicing)] // `i <= modes.len()` checked above
1401+
let terminal_modes = modes[..i].to_vec();
13971402
let _ = chan
13981403
.send(ChannelMsg::RequestPty {
13991404
want_reply: true,
@@ -1402,7 +1407,7 @@ impl Session {
14021407
row_height,
14031408
pix_width,
14041409
pix_height,
1405-
terminal_modes: modes.into(),
1410+
terminal_modes,
14061411
})
14071412
.await;
14081413
}

russh/src/tests.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,104 @@ mod channels {
683683
)
684684
.await;
685685
}
686+
687+
/// What a client sends in a pty-req is what the application on the server
688+
/// side must see: exactly those modes, with no padding behind them.
689+
#[tokio::test]
690+
async fn test_pty_req_modes_are_delivered_unpadded() {
691+
let modes = vec![(Pty::VINTR, 3), (Pty::VQUIT, 28)];
692+
assert_eq!(send_pty_req(modes.clone()).await, modes);
693+
}
694+
695+
/// A mode list padded with TTY_OP_END entries -- the shape an application
696+
/// proxying a pty-req ends up holding -- must still produce a well formed
697+
/// request. The encoder skips those entries, so the length it declares for
698+
/// the modes string must not count them, or the receiver reads the rest of
699+
/// the packet as terminal modes and rejects it.
700+
#[tokio::test]
701+
async fn test_pty_req_with_padded_modes_is_well_formed() {
702+
let mut padded = vec![(Pty::VINTR, 3)];
703+
padded.resize(130, (Pty::TTY_OP_END, 0));
704+
assert_eq!(send_pty_req(padded).await, vec![(Pty::VINTR, 3)]);
705+
}
706+
707+
/// Send one pty-req and return the modes the server side received.
708+
async fn send_pty_req(modes: Vec<(Pty, u32)>) -> Vec<(Pty, u32)> {
709+
struct Client {}
710+
711+
impl client::Handler for Client {
712+
type Error = crate::Error;
713+
714+
async fn check_server_key(
715+
&mut self,
716+
_server_public_key: &PublicKeyOrCertificate,
717+
) -> Result<bool, Self::Error> {
718+
Ok(true)
719+
}
720+
}
721+
722+
struct ServerHandle {
723+
seen: tokio::sync::mpsc::UnboundedSender<Vec<(Pty, u32)>>,
724+
}
725+
726+
impl server::Handler for ServerHandle {
727+
type Error = crate::Error;
728+
729+
async fn auth_publickey(
730+
&mut self,
731+
_: &str,
732+
_: &crate::keys::ssh_key::PublicKey,
733+
) -> Result<server::Auth, Self::Error> {
734+
Ok(server::Auth::Accept)
735+
}
736+
737+
async fn channel_open_session(
738+
&mut self,
739+
mut channel: Channel<server::Msg>,
740+
reply: server::ChannelOpenHandle,
741+
session: &mut Session,
742+
) -> Result<(), Self::Error> {
743+
reply.accept().await;
744+
745+
let seen = self.seen.clone();
746+
let handle = session.handle();
747+
let id = channel.id();
748+
tokio::spawn(async move {
749+
while let Some(msg) = channel.wait().await {
750+
if let ChannelMsg::RequestPty { terminal_modes, .. } = msg {
751+
let _ = seen.send(terminal_modes);
752+
// Answer, so the client knows we are done looking.
753+
let _ = handle.channel_success(id).await;
754+
break;
755+
}
756+
}
757+
});
758+
Ok(())
759+
}
760+
}
761+
762+
let (seen, mut received) = tokio::sync::mpsc::unbounded_channel();
763+
test_session(
764+
Client {},
765+
ServerHandle { seen },
766+
move |client| async move {
767+
let mut channel = client.channel_open_session().await.unwrap();
768+
channel
769+
.request_pty(true, "xterm", 80, 24, 0, 0, &modes)
770+
.await
771+
.unwrap();
772+
match channel.wait().await {
773+
Some(ChannelMsg::Success) => (),
774+
other => panic!("pty request was not accepted: {other:?}"),
775+
}
776+
client
777+
},
778+
|server| async move { server },
779+
)
780+
.await;
781+
782+
received.try_recv().expect("server never saw the pty request")
783+
}
686784
}
687785

688786
mod gex {

0 commit comments

Comments
 (0)