Complete SSH transport release audit (#118)
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
Some checks failed
Dependency security audit / rustsec (push) Has been cancelled
This commit is contained in:
@@ -545,66 +545,69 @@ fn assert_pack_request(request: &[u8], old: Option<&str>, new: &str) {
|
||||
|
||||
#[test]
|
||||
fn new_branch_fast_forward_and_already_current_push_are_confirmed() {
|
||||
for (old_kind, behavior) in [
|
||||
(None, ReceiveBehavior::Success),
|
||||
(Some("base"), ReceiveBehavior::Success),
|
||||
] {
|
||||
for scp_like in [false, true] {
|
||||
for old_kind in [None, Some("base")] {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let local = local_repository(&temporary.path().join("local"));
|
||||
let base = commit(&local, ".gpg-id", b"ALICE\n", "Initialize");
|
||||
let new = commit(&local, "entry.gpg", b"ciphertext", "Add entry");
|
||||
let old = old_kind.map(|_| base.clone());
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
None,
|
||||
old.clone(),
|
||||
ReceiveBehavior::Success,
|
||||
false,
|
||||
1,
|
||||
);
|
||||
let remote = remote(temporary.path(), &server, &user_key, scp_like);
|
||||
let mut local = local;
|
||||
local.add_remote("origin", remote.url()).expect("remote");
|
||||
let outcome = local
|
||||
.push(&remote, Some("main"), &Credentials)
|
||||
.expect("push");
|
||||
assert_eq!(outcome.new_id(), new);
|
||||
assert_eq!(tracking_id(&local), Some(new.clone()));
|
||||
server.join.join().expect("server");
|
||||
let observed = server.observed.lock().expect("observed");
|
||||
let expected = if scp_like {
|
||||
b"git-receive-pack 'team/store.git'".to_vec()
|
||||
} else {
|
||||
b"git-receive-pack '/team/store.git'".to_vec()
|
||||
};
|
||||
assert_eq!(observed.commands, [expected]);
|
||||
assert_eq!(observed.receive_requests.len(), 1);
|
||||
assert_pack_request(&observed.receive_requests[0], old.as_deref(), &new);
|
||||
}
|
||||
}
|
||||
|
||||
for scp_like in [false, true] {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let local = local_repository(&temporary.path().join("local"));
|
||||
let base = commit(&local, ".gpg-id", b"ALICE\n", "Initialize");
|
||||
let new = commit(&local, "entry.gpg", b"ciphertext", "Add entry");
|
||||
let old = old_kind.map(|_| base.clone());
|
||||
let head = commit(&local, ".gpg-id", b"ALICE\n", "Initialize");
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
None,
|
||||
old.clone(),
|
||||
behavior,
|
||||
Some(head.clone()),
|
||||
ReceiveBehavior::Success,
|
||||
false,
|
||||
1,
|
||||
);
|
||||
let remote = remote(temporary.path(), &server, &user_key, false);
|
||||
let remote = remote(temporary.path(), &server, &user_key, scp_like);
|
||||
let mut local = local;
|
||||
local.add_remote("origin", remote.url()).expect("remote");
|
||||
let outcome = local
|
||||
local
|
||||
.push(&remote, Some("main"), &Credentials)
|
||||
.expect("push");
|
||||
assert_eq!(outcome.new_id(), new);
|
||||
assert_eq!(tracking_id(&local), Some(new.clone()));
|
||||
.expect("current push");
|
||||
assert_eq!(tracking_id(&local), Some(head));
|
||||
server.join.join().expect("server");
|
||||
let observed = server.observed.lock().expect("observed");
|
||||
assert_eq!(
|
||||
observed.commands,
|
||||
[b"git-receive-pack '/team/store.git'".to_vec()]
|
||||
server.observed.lock().expect("observed").receive_requests,
|
||||
[b"0000".to_vec()]
|
||||
);
|
||||
assert_eq!(observed.receive_requests.len(), 1);
|
||||
assert_pack_request(&observed.receive_requests[0], old.as_deref(), &new);
|
||||
}
|
||||
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let local = local_repository(&temporary.path().join("local"));
|
||||
let head = commit(&local, ".gpg-id", b"ALICE\n", "Initialize");
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
None,
|
||||
Some(head.clone()),
|
||||
ReceiveBehavior::Success,
|
||||
false,
|
||||
1,
|
||||
);
|
||||
let remote = remote(temporary.path(), &server, &user_key, false);
|
||||
let mut local = local;
|
||||
local.add_remote("origin", remote.url()).expect("remote");
|
||||
local
|
||||
.push(&remote, Some("main"), &Credentials)
|
||||
.expect("current push");
|
||||
assert_eq!(tracking_id(&local), Some(head));
|
||||
server.join.join().expect("server");
|
||||
assert_eq!(
|
||||
server.observed.lock().expect("observed").receive_requests,
|
||||
[b"0000".to_vec()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -377,155 +377,111 @@ fn remote_with_url(root: &Path, server: &Server, identity: &PrivateKey, url: Str
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_pack_drives_discovery_clone_fetch_fast_forward_merge_and_conflict() {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let source_root = temporary.path().join("source");
|
||||
fs::create_dir(&source_root).expect("source directory");
|
||||
let source_store = Repository::open(&source_root).expect("source store");
|
||||
let source = GitRepository::init(&source_store, identity()).expect("source Git");
|
||||
commit(&source, ".gpg-id", b"ALICE\n", "Initialize recipients");
|
||||
commit(&source, "shared.gpg", b"base", "Add shared entry");
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
populated_fixture(&source),
|
||||
Behavior::Normal,
|
||||
5,
|
||||
);
|
||||
let remote = remote(temporary.path(), &server, &user_key, "/team/store.git");
|
||||
fn both_ssh_url_forms_drive_discovery_clone_fetch_fast_forward_merge_and_conflict() {
|
||||
for scp_like in [false, true] {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let source_root = temporary.path().join("source");
|
||||
fs::create_dir(&source_root).expect("source directory");
|
||||
let source_store = Repository::open(&source_root).expect("source store");
|
||||
let source = GitRepository::init(&source_store, identity()).expect("source Git");
|
||||
commit(&source, ".gpg-id", b"ALICE\n", "Initialize recipients");
|
||||
commit(&source, "shared.gpg", b"base", "Add shared entry");
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
populated_fixture(&source),
|
||||
Behavior::Normal,
|
||||
6,
|
||||
);
|
||||
let remote = if scp_like {
|
||||
remote_with_url(
|
||||
temporary.path(),
|
||||
&server,
|
||||
&user_key,
|
||||
"git@127.0.0.1:team/store.git".to_owned(),
|
||||
)
|
||||
} else {
|
||||
remote(temporary.path(), &server, &user_key, "/team/store.git")
|
||||
};
|
||||
|
||||
let branches = GitRepository::discover_remote_branches(
|
||||
temporary.path(),
|
||||
identity(),
|
||||
&remote,
|
||||
&Credentials,
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
.expect("discover branches");
|
||||
assert_eq!(branches, ["feature", "main"]);
|
||||
|
||||
let destination = temporary.path().join("clone");
|
||||
let clone = GitRepository::clone_into(&destination, identity(), &remote, &Credentials)
|
||||
.expect("clone over SSH");
|
||||
assert_eq!(
|
||||
fs::read(destination.join("shared.gpg")).expect("entry"),
|
||||
b"base"
|
||||
);
|
||||
assert!(
|
||||
clone
|
||||
.repository
|
||||
.try_find_reference("refs/tags/v1")
|
||||
.expect("tag lookup")
|
||||
.is_some()
|
||||
);
|
||||
|
||||
commit(&source, "remote-one.gpg", b"remote", "Remote fast-forward");
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
assert_eq!(
|
||||
clone
|
||||
.pull(&remote, Some("main"), &Credentials)
|
||||
.expect("fast-forward pull"),
|
||||
PullOutcome::FastForward
|
||||
);
|
||||
|
||||
commit(&clone, "local.gpg", b"local", "Local change");
|
||||
commit(
|
||||
&source,
|
||||
"remote-two.gpg",
|
||||
b"remote",
|
||||
"Remote parallel change",
|
||||
);
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
assert_eq!(
|
||||
clone
|
||||
.pull(&remote, Some("main"), &Credentials)
|
||||
.expect("merge pull"),
|
||||
PullOutcome::Merged
|
||||
);
|
||||
|
||||
commit(&clone, "shared.gpg", b"local conflict", "Local conflict");
|
||||
commit(&source, "shared.gpg", b"remote conflict", "Remote conflict");
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
let before = fs::read(destination.join("shared.gpg")).expect("local entry");
|
||||
assert!(matches!(
|
||||
clone.pull(&remote, Some("main"), &Credentials),
|
||||
Err(GitError::MergeConflicts { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
fs::read(destination.join("shared.gpg")).expect("entry"),
|
||||
before
|
||||
);
|
||||
|
||||
server.join.join().expect("server");
|
||||
let commands = server.commands.lock().expect("commands");
|
||||
assert_eq!(commands.len(), 5);
|
||||
assert!(
|
||||
commands
|
||||
.iter()
|
||||
.all(|command| command == b"git-upload-pack '/team/store.git'")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scp_remote_drives_discovery_clone_fetch_and_pull() {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let source_root = temporary.path().join("source");
|
||||
fs::create_dir(&source_root).expect("source directory");
|
||||
let source_store = Repository::open(&source_root).expect("source store");
|
||||
let source = GitRepository::init(&source_store, identity()).expect("source Git");
|
||||
commit(&source, ".gpg-id", b"ALICE\n", "Initialize recipients");
|
||||
let user_key = key();
|
||||
let server = start_server(
|
||||
user_key.public_key().clone(),
|
||||
populated_fixture(&source),
|
||||
Behavior::Normal,
|
||||
4,
|
||||
);
|
||||
let remote = remote_with_url(
|
||||
temporary.path(),
|
||||
&server,
|
||||
&user_key,
|
||||
"git@127.0.0.1:team/store.git".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
GitRepository::discover_remote_branches(
|
||||
let branches = GitRepository::discover_remote_branches(
|
||||
temporary.path(),
|
||||
identity(),
|
||||
&remote,
|
||||
&Credentials,
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
.expect("discover branches"),
|
||||
["feature", "main"]
|
||||
);
|
||||
let destination = temporary.path().join("clone");
|
||||
let clone = GitRepository::clone_into(&destination, identity(), &remote, &Credentials)
|
||||
.expect("clone over scp-like SSH remote");
|
||||
commit(&source, "remote.gpg", b"remote", "Remote fast-forward");
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
assert!(
|
||||
clone
|
||||
.fetch(&remote, &Credentials)
|
||||
.expect("fetch over scp-like SSH remote")
|
||||
.received_pack()
|
||||
);
|
||||
assert_eq!(
|
||||
clone
|
||||
.pull(&remote, Some("main"), &Credentials)
|
||||
.expect("pull over scp-like SSH remote"),
|
||||
PullOutcome::FastForward
|
||||
);
|
||||
.expect("discover branches");
|
||||
assert_eq!(branches, ["feature", "main"]);
|
||||
|
||||
server.join.join().expect("server");
|
||||
assert!(
|
||||
server
|
||||
.commands
|
||||
.lock()
|
||||
.expect("commands")
|
||||
.iter()
|
||||
.all(|command| command == b"git-upload-pack 'team/store.git'")
|
||||
);
|
||||
let destination = temporary.path().join("clone");
|
||||
let clone = GitRepository::clone_into(&destination, identity(), &remote, &Credentials)
|
||||
.expect("clone over SSH");
|
||||
assert_eq!(
|
||||
fs::read(destination.join("shared.gpg")).expect("entry"),
|
||||
b"base"
|
||||
);
|
||||
assert!(
|
||||
clone
|
||||
.repository
|
||||
.try_find_reference("refs/tags/v1")
|
||||
.expect("tag lookup")
|
||||
.is_some()
|
||||
);
|
||||
|
||||
commit(&source, "remote-one.gpg", b"remote", "Remote fast-forward");
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
assert!(
|
||||
clone
|
||||
.fetch(&remote, &Credentials)
|
||||
.expect("fetch over SSH")
|
||||
.received_pack()
|
||||
);
|
||||
assert_eq!(
|
||||
clone
|
||||
.pull(&remote, Some("main"), &Credentials)
|
||||
.expect("fast-forward pull"),
|
||||
PullOutcome::FastForward
|
||||
);
|
||||
|
||||
commit(&clone, "local.gpg", b"local", "Local change");
|
||||
commit(
|
||||
&source,
|
||||
"remote-two.gpg",
|
||||
b"remote",
|
||||
"Remote parallel change",
|
||||
);
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
assert_eq!(
|
||||
clone
|
||||
.pull(&remote, Some("main"), &Credentials)
|
||||
.expect("merge pull"),
|
||||
PullOutcome::Merged
|
||||
);
|
||||
|
||||
commit(&clone, "shared.gpg", b"local conflict", "Local conflict");
|
||||
commit(&source, "shared.gpg", b"remote conflict", "Remote conflict");
|
||||
*server.fixture.lock().expect("fixture") = populated_fixture(&source);
|
||||
let before = fs::read(destination.join("shared.gpg")).expect("local entry");
|
||||
assert!(matches!(
|
||||
clone.pull(&remote, Some("main"), &Credentials),
|
||||
Err(GitError::MergeConflicts { .. })
|
||||
));
|
||||
assert_eq!(
|
||||
fs::read(destination.join("shared.gpg")).expect("entry"),
|
||||
before
|
||||
);
|
||||
|
||||
server.join.join().expect("server");
|
||||
let commands = server.commands.lock().expect("commands");
|
||||
assert_eq!(commands.len(), 6);
|
||||
let expected = if scp_like {
|
||||
b"git-upload-pack 'team/store.git'".as_slice()
|
||||
} else {
|
||||
b"git-upload-pack '/team/store.git'".as_slice()
|
||||
};
|
||||
assert!(commands.iter().all(|command| command == expected));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -14,11 +14,12 @@ use cap_std::{ambient_authority, fs::Dir};
|
||||
use cap_tempfile::TempFile;
|
||||
use hmac::{Hmac, Mac as _};
|
||||
use russh::{
|
||||
ChannelMsg, Disconnect, client,
|
||||
ChannelMsg, Disconnect, Preferred, cipher, client, compression, kex,
|
||||
keys::{
|
||||
HashAlg, PrivateKey, PublicKey, agent::client::AgentClient, key::PrivateKeyWithHashAlg,
|
||||
ssh_key::Algorithm,
|
||||
},
|
||||
mac,
|
||||
};
|
||||
use sha1::Sha1;
|
||||
|
||||
@@ -140,19 +141,7 @@ impl SshSession {
|
||||
port,
|
||||
known_hosts,
|
||||
};
|
||||
let mut config = client::Config {
|
||||
inactivity_timeout: Some(CONNECTION_TIMEOUT),
|
||||
..client::Config::default()
|
||||
};
|
||||
config.preferred.key = Cow::Owned(
|
||||
config
|
||||
.preferred
|
||||
.key
|
||||
.iter()
|
||||
.filter(|algorithm| !matches!(algorithm, Algorithm::Rsa { hash: None }))
|
||||
.cloned()
|
||||
.collect(),
|
||||
);
|
||||
let config = client_config();
|
||||
let handle = runtime.block_on(async {
|
||||
controlled(
|
||||
tokio::time::timeout(
|
||||
@@ -300,6 +289,61 @@ impl SshSession {
|
||||
}
|
||||
}
|
||||
|
||||
fn client_config() -> client::Config {
|
||||
client::Config {
|
||||
inactivity_timeout: Some(CONNECTION_TIMEOUT),
|
||||
preferred: Preferred {
|
||||
kex: Cow::Owned(vec![
|
||||
kex::MLKEM768X25519_SHA256,
|
||||
kex::CURVE25519,
|
||||
kex::CURVE25519_PRE_RFC_8731,
|
||||
kex::DH_GEX_SHA256,
|
||||
kex::DH_G18_SHA512,
|
||||
kex::DH_G17_SHA512,
|
||||
kex::DH_G16_SHA512,
|
||||
kex::DH_G15_SHA512,
|
||||
kex::DH_G14_SHA256,
|
||||
kex::EXTENSION_SUPPORT_AS_CLIENT,
|
||||
kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT,
|
||||
]),
|
||||
host_key_certificates: Cow::Borrowed(&[]),
|
||||
key: Cow::Owned(vec![
|
||||
Algorithm::Ed25519,
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP256,
|
||||
},
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP384,
|
||||
},
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP521,
|
||||
},
|
||||
Algorithm::Rsa {
|
||||
hash: Some(HashAlg::Sha512),
|
||||
},
|
||||
Algorithm::Rsa {
|
||||
hash: Some(HashAlg::Sha256),
|
||||
},
|
||||
]),
|
||||
cipher: Cow::Owned(vec![
|
||||
cipher::CHACHA20_POLY1305,
|
||||
cipher::AES_256_GCM,
|
||||
cipher::AES_256_CTR,
|
||||
cipher::AES_192_CTR,
|
||||
cipher::AES_128_CTR,
|
||||
]),
|
||||
mac: Cow::Owned(vec![
|
||||
mac::HMAC_SHA512_ETM,
|
||||
mac::HMAC_SHA256_ETM,
|
||||
mac::HMAC_SHA512,
|
||||
mac::HMAC_SHA256,
|
||||
]),
|
||||
compression: Cow::Owned(vec![compression::NONE]),
|
||||
},
|
||||
..client::Config::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn open_command_channel(
|
||||
handle: &client::Handle<HostVerifier>,
|
||||
command: Vec<u8>,
|
||||
@@ -983,7 +1027,7 @@ mod tests {
|
||||
|
||||
use russh::{
|
||||
keys::{
|
||||
PrivateKey, PublicKey,
|
||||
HashAlg, PrivateKey, PublicKey,
|
||||
agent::client::AgentClient,
|
||||
ssh_key::{Algorithm, LineEnding},
|
||||
},
|
||||
@@ -997,7 +1041,8 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::{
|
||||
GitService, SshSession, git_service_command, persist_confirmed_host, ssh_host_key,
|
||||
GitService, SshSession, client_config, git_service_command, persist_confirmed_host,
|
||||
ssh_host_key,
|
||||
};
|
||||
|
||||
struct Passphrase(Option<&'static [u8]>);
|
||||
@@ -1143,13 +1188,19 @@ mod tests {
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP256,
|
||||
},
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP384,
|
||||
},
|
||||
Algorithm::Ecdsa {
|
||||
curve: russh::keys::ssh_key::EcdsaCurve::NistP521,
|
||||
},
|
||||
Algorithm::Rsa {
|
||||
hash: Some(russh::keys::HashAlg::Sha512),
|
||||
},
|
||||
] {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let identity = key(algorithm);
|
||||
let host_key = key(Algorithm::Ed25519);
|
||||
let identity = key(algorithm.clone());
|
||||
let host_key = key(algorithm);
|
||||
let identity_path = temporary.path().join("identity");
|
||||
let known_hosts = temporary.path().join("known_hosts");
|
||||
write_key(&identity_path, &identity);
|
||||
@@ -1172,6 +1223,47 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_algorithm_policy_excludes_legacy_ssh_primitives() {
|
||||
let config = client_config();
|
||||
assert!(
|
||||
config
|
||||
.preferred
|
||||
.kex
|
||||
.iter()
|
||||
.all(|name| !name.as_ref().contains("sha1"))
|
||||
);
|
||||
assert!(config.preferred.host_key_certificates.is_empty());
|
||||
assert!(config.preferred.key.iter().all(|algorithm| matches!(
|
||||
algorithm,
|
||||
Algorithm::Ed25519
|
||||
| Algorithm::Ecdsa { .. }
|
||||
| Algorithm::Rsa {
|
||||
hash: Some(HashAlg::Sha256 | HashAlg::Sha512)
|
||||
}
|
||||
)));
|
||||
assert!(config.preferred.cipher.iter().all(|name| {
|
||||
let name = name.as_ref();
|
||||
!name.contains("cbc") && name != "none"
|
||||
}));
|
||||
assert!(
|
||||
config
|
||||
.preferred
|
||||
.mac
|
||||
.iter()
|
||||
.all(|name| !name.as_ref().contains("sha1"))
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.preferred
|
||||
.compression
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.collect::<Vec<_>>(),
|
||||
["none"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypted_key_requires_the_matching_protected_passphrase() {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
@@ -1331,7 +1423,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_network_is_distinct_from_ssh_protocol_failure() {
|
||||
fn dns_and_network_failures_are_distinct_from_ssh_protocol_failure() {
|
||||
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||
let identity = key(Algorithm::Ed25519);
|
||||
let identity_path = temporary.path().join("identity");
|
||||
@@ -1350,6 +1442,23 @@ mod tests {
|
||||
.expect_err("unavailable network"),
|
||||
GitError::NetworkUnavailable
|
||||
);
|
||||
|
||||
let dns_remote = GitRemote::ssh_with_authentication(
|
||||
"origin",
|
||||
"ssh://git@does-not-exist.invalid/team/store.git",
|
||||
SshRemoteAuthentication::key_file(identity_path, known_hosts)
|
||||
.expect("SSH authentication"),
|
||||
)
|
||||
.expect("SSH remote");
|
||||
assert_eq!(
|
||||
SshSession::connect(
|
||||
&dns_remote,
|
||||
&Passphrase(None),
|
||||
&GitOperationControl::default(),
|
||||
)
|
||||
.expect_err("unavailable DNS name"),
|
||||
GitError::NetworkUnavailable
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user