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]
|
||||
|
||||
@@ -208,12 +208,17 @@ fn ssh_remotes_are_typed_but_unavailable_before_transport_or_mutation() -> TestR
|
||||
|
||||
#[cfg(feature = "ssh")]
|
||||
#[test]
|
||||
fn ssh_feature_allows_repository_remote_configuration() -> TestResult {
|
||||
fn ssh_feature_allows_both_remote_forms_through_add_set_and_get() -> TestResult {
|
||||
let temporary = tempfile::tempdir()?;
|
||||
let store = Repository::open(temporary.path())?;
|
||||
let mut git = GitRepository::init(&store, identity())?;
|
||||
git.add_remote("origin", "git@example.test:team/store.git")?;
|
||||
assert_eq!(git.remote_url("origin")?, "git@example.test:team/store.git");
|
||||
git.set_remote_url("origin", "ssh://git@example.test/team/store.git")?;
|
||||
assert_eq!(
|
||||
git.remote_url("origin")?,
|
||||
"ssh://git@example.test/team/store.git"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
53
crates/storage/tests/ssh_release_audit.rs
Normal file
53
crates/storage/tests/ssh_release_audit.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
const WORKSPACE: &str = include_str!("../../../Cargo.toml");
|
||||
const STORAGE_MANIFEST: &str = include_str!("../Cargo.toml");
|
||||
const APPLE_MANIFEST: &str = include_str!("../../apple/Cargo.toml");
|
||||
const CLI_MANIFEST: &str = include_str!("../../../apps/cli/Cargo.toml");
|
||||
const TUI_MANIFEST: &str = include_str!("../../../apps/tui/Cargo.toml");
|
||||
const DESKTOP_MANIFEST: &str = include_str!("../../../apps/desktop/Cargo.toml");
|
||||
const SSH_SOURCE: &str = include_str!("../src/ssh.rs");
|
||||
const GIT_SOURCE: &str = include_str!("../src/git.rs");
|
||||
const AUDIT: &str = include_str!("../../../docs/ssh-transport-audit.md");
|
||||
const AUDIT_WORKFLOW: &str = include_str!("../../../.gitea/workflows/security-audit.yml");
|
||||
|
||||
#[test]
|
||||
fn release_feature_and_advisory_boundaries_stay_explicit() {
|
||||
assert!(WORKSPACE.contains(
|
||||
"russh = { version = \"=0.63.1\", default-features = false, features = [\"ring\", \"rsa\"] }"
|
||||
));
|
||||
assert!(STORAGE_MANIFEST.contains("ssh = [\"dep:russh\", \"dep:tokio\"]"));
|
||||
for manifest in [CLI_MANIFEST, TUI_MANIFEST, DESKTOP_MANIFEST] {
|
||||
assert!(manifest.contains("features = [\"ssh\"]"));
|
||||
}
|
||||
assert!(APPLE_MANIFEST.contains("default-features = false, features = [\"full\"]"));
|
||||
assert!(!APPLE_MANIFEST.contains("features = [\"ssh\"]"));
|
||||
assert!(AUDIT_WORKFLOW.contains("cargo-audit --locked --version 0.22.2"));
|
||||
assert!(AUDIT_WORKFLOW.contains("cargo audit --ignore RUSTSEC-2023-0071"));
|
||||
assert!(AUDIT.contains("RUSTSEC-2023-0071"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn production_ssh_transport_has_no_process_unsafe_or_proxy_escape_hatch() {
|
||||
let process_command = ["process", "::Command"].concat();
|
||||
let command_constructor = ["Command", "::new("].concat();
|
||||
let unsafe_block = ["unsafe", " {"].concat();
|
||||
for (name, source) in [("ssh.rs", SSH_SOURCE), ("git.rs", GIT_SOURCE)] {
|
||||
let production = source.split("#[cfg(test)]").next().unwrap_or(source);
|
||||
for forbidden in [
|
||||
process_command.as_str(),
|
||||
command_constructor.as_str(),
|
||||
unsafe_block.as_str(),
|
||||
"ProxyCommand",
|
||||
"proxy_command",
|
||||
"russh_config",
|
||||
] {
|
||||
assert!(
|
||||
!production.contains(forbidden),
|
||||
"{name} contains forbidden production token {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(SSH_SOURCE.contains("check_server_key"));
|
||||
assert!(SSH_SOURCE.contains("verify_known_host"));
|
||||
assert!(SSH_SOURCE.contains("MAX_SSH_DIAGNOSTIC_BYTES"));
|
||||
assert!(SSH_SOURCE.contains("MAX_CHANNEL_CHUNK"));
|
||||
}
|
||||
Reference in New Issue
Block a user