Use repository Git remotes and SSH config
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:
@@ -13,7 +13,11 @@ use std::{
|
|||||||
|
|
||||||
use cap_std::{ambient_authority, fs::Dir};
|
use cap_std::{ambient_authority, fs::Dir};
|
||||||
use cap_tempfile::TempFile;
|
use cap_tempfile::TempFile;
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
use gix::bstr::ByteSlice as _;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
use sha2::{Digest as _, Sha256};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -1694,7 +1698,14 @@ fn validate_config(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let git_remotes = validate_remotes(raw.git.remotes, Some(base))?;
|
let configured_git_remotes = raw.git.remotes;
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
let git_remotes = match repository_git_remotes(&vault)? {
|
||||||
|
Some(remotes) => remotes,
|
||||||
|
None => validate_remotes(configured_git_remotes, Some(base))?,
|
||||||
|
};
|
||||||
|
#[cfg(any(target_os = "ios", target_os = "watchos"))]
|
||||||
|
let git_remotes = validate_remotes(configured_git_remotes, Some(base))?;
|
||||||
|
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
source,
|
source,
|
||||||
@@ -1715,6 +1726,75 @@ fn validate_config(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
fn repository_git_remotes(vault: &Path) -> Result<Option<Vec<GitRemote>>, ConfigError> {
|
||||||
|
let path = vault.join(".git/config");
|
||||||
|
if !path.is_file() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let config =
|
||||||
|
gix_config::File::from_path_no_includes(path, gix_config::Source::Local).map_err(|_| {
|
||||||
|
ConfigError::InvalidField {
|
||||||
|
field: "git.repository_remotes",
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
let mut remotes = Vec::new();
|
||||||
|
if let Some(sections) = config.sections_by_name("remote") {
|
||||||
|
for section in sections {
|
||||||
|
let name = section
|
||||||
|
.header()
|
||||||
|
.subsection_name()
|
||||||
|
.and_then(|name| name.to_str().ok())
|
||||||
|
.ok_or(ConfigError::InvalidField {
|
||||||
|
field: "git.repository_remotes",
|
||||||
|
})?;
|
||||||
|
let url = config
|
||||||
|
.raw_value_by("remote", Some(name.into()), "url")
|
||||||
|
.ok()
|
||||||
|
.and_then(|url| url.to_str().ok().map(str::to_owned))
|
||||||
|
.ok_or(ConfigError::InvalidRemoteUrl {
|
||||||
|
name: name.to_owned(),
|
||||||
|
})?;
|
||||||
|
let endpoint =
|
||||||
|
RemoteEndpoint::parse(&url).map_err(|_| ConfigError::InvalidRemoteUrl {
|
||||||
|
name: name.to_owned(),
|
||||||
|
})?;
|
||||||
|
let remote = match endpoint {
|
||||||
|
RemoteEndpoint::Https(ref endpoint) => GitRemote::https(
|
||||||
|
name,
|
||||||
|
&url,
|
||||||
|
stable_identifier(
|
||||||
|
"server",
|
||||||
|
format!(
|
||||||
|
"{}://{}:{}",
|
||||||
|
endpoint.scheme(),
|
||||||
|
endpoint.host_str().unwrap_or_default(),
|
||||||
|
endpoint.port_or_known_default().unwrap_or(443)
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
),
|
||||||
|
stable_identifier("repository", url.as_bytes()),
|
||||||
|
)?,
|
||||||
|
RemoteEndpoint::Ssh(_) => GitRemote::ssh(name, &url)?,
|
||||||
|
};
|
||||||
|
remotes.push(remote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((!remotes.is_empty()).then_some(remotes))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
fn stable_identifier(prefix: &str, value: &[u8]) -> String {
|
||||||
|
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||||
|
let digest = Sha256::digest(value);
|
||||||
|
let suffix: String = digest[..8]
|
||||||
|
.iter()
|
||||||
|
.flat_map(|byte| [HEX[(byte >> 4) as usize], HEX[(byte & 0x0f) as usize]])
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
format!("{prefix}-{suffix}")
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_required_path(
|
fn resolve_required_path(
|
||||||
base: &Path,
|
base: &Path,
|
||||||
value: Option<PathBuf>,
|
value: Option<PathBuf>,
|
||||||
|
|||||||
@@ -892,13 +892,11 @@ pub fn confirm_ssh_host(remote: &GitRemote, host_key: &SshHostKey) -> Result<(),
|
|||||||
.endpoint()
|
.endpoint()
|
||||||
.as_ssh()
|
.as_ssh()
|
||||||
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||||
if endpoint.host() != host_key.host() || endpoint.port() != host_key.port() {
|
let connection = crate::ssh::resolve_connection(endpoint, remote.ssh_authentication())?;
|
||||||
|
if connection.host != host_key.host() || connection.port != host_key.port() {
|
||||||
return Err(GitError::SshProtocolFailed);
|
return Err(GitError::SshProtocolFailed);
|
||||||
}
|
}
|
||||||
let authentication = remote
|
crate::ssh::persist_confirmed_host(&connection.known_hosts, host_key)
|
||||||
.ssh_authentication()
|
|
||||||
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
|
||||||
crate::ssh::persist_confirmed_host(authentication.known_hosts_file(), host_key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait GitSmartHttpTransport {
|
pub trait GitSmartHttpTransport {
|
||||||
|
|||||||
@@ -24,7 +24,10 @@ use russh::{
|
|||||||
use sha1::Sha1;
|
use sha1::Sha1;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{GitRemote, RemoteEndpoint, SshFingerprint, SshIdentitySource, SshRepositoryPath},
|
config::{
|
||||||
|
GitRemote, RemoteEndpoint, SshEndpoint, SshFingerprint, SshIdentitySource,
|
||||||
|
SshRemoteAuthentication, SshRepositoryPath,
|
||||||
|
},
|
||||||
git::{GitError, GitOperationControl, GitProgressPhase, SshHostKey, SshPassphraseProvider},
|
git::{GitError, GitOperationControl, GitProgressPhase, SshHostKey, SshPassphraseProvider},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,18 +127,16 @@ impl SshSession {
|
|||||||
let RemoteEndpoint::Ssh(endpoint) = remote.endpoint() else {
|
let RemoteEndpoint::Ssh(endpoint) = remote.endpoint() else {
|
||||||
return Err(GitError::ForbiddenRemoteUrl);
|
return Err(GitError::ForbiddenRemoteUrl);
|
||||||
};
|
};
|
||||||
let authentication = remote
|
let connection = resolve_connection(endpoint, remote.ssh_authentication())?;
|
||||||
.ssh_authentication()
|
|
||||||
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
|
||||||
control.report(GitProgressPhase::Validating)?;
|
control.report(GitProgressPhase::Validating)?;
|
||||||
|
|
||||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
.map_err(|_| GitError::SshProtocolFailed)?;
|
.map_err(|_| GitError::SshProtocolFailed)?;
|
||||||
let host = endpoint.host().to_owned();
|
let host = connection.host.clone();
|
||||||
let port = endpoint.port();
|
let port = connection.port;
|
||||||
let known_hosts = authentication.known_hosts_file().to_owned();
|
let known_hosts = connection.known_hosts.clone();
|
||||||
let verifier = HostVerifier {
|
let verifier = HostVerifier {
|
||||||
host: host.clone(),
|
host: host.clone(),
|
||||||
port,
|
port,
|
||||||
@@ -159,8 +160,8 @@ impl SshSession {
|
|||||||
};
|
};
|
||||||
|
|
||||||
control.report(GitProgressPhase::Authenticating)?;
|
control.report(GitProgressPhase::Authenticating)?;
|
||||||
let user = endpoint.user().unwrap_or("git");
|
let user = connection.user.as_deref().unwrap_or("git");
|
||||||
let authenticated = match authentication.identity() {
|
let authenticated = match &connection.identity {
|
||||||
SshIdentitySource::KeyFile(path) => runtime.block_on(authenticate_key_file(
|
SshIdentitySource::KeyFile(path) => runtime.block_on(authenticate_key_file(
|
||||||
&mut handle,
|
&mut handle,
|
||||||
user,
|
user,
|
||||||
@@ -289,6 +290,150 @@ impl SshSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SshConnection {
|
||||||
|
pub(crate) host: String,
|
||||||
|
pub(crate) port: u16,
|
||||||
|
user: Option<String>,
|
||||||
|
identity: SshIdentitySource,
|
||||||
|
pub(crate) known_hosts: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_connection(
|
||||||
|
endpoint: &SshEndpoint,
|
||||||
|
authentication: Option<&SshRemoteAuthentication>,
|
||||||
|
) -> Result<SshConnection, GitError> {
|
||||||
|
let home = home_directory().ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||||
|
let mut connection = SshConnection {
|
||||||
|
host: endpoint.host().to_owned(),
|
||||||
|
port: endpoint.port(),
|
||||||
|
user: endpoint.user().map(str::to_owned),
|
||||||
|
identity: authentication
|
||||||
|
.map(|authentication| authentication.identity().clone())
|
||||||
|
.unwrap_or_else(|| SshIdentitySource::KeyFile(home.join(".ssh/id_ed25519"))),
|
||||||
|
known_hosts: authentication
|
||||||
|
.map(|authentication| authentication.known_hosts_file().to_owned())
|
||||||
|
.unwrap_or_else(|| home.join(".ssh/known_hosts")),
|
||||||
|
};
|
||||||
|
apply_openssh_config(
|
||||||
|
&home.join(".ssh/config"),
|
||||||
|
endpoint.host(),
|
||||||
|
&home,
|
||||||
|
authentication.is_none(),
|
||||||
|
&mut connection,
|
||||||
|
)?;
|
||||||
|
Ok(connection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn home_directory() -> Option<PathBuf> {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let home = std::env::var_os("USERPROFILE");
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let home = std::env::var_os("HOME");
|
||||||
|
home.filter(|home| !home.is_empty()).map(PathBuf::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_openssh_config(
|
||||||
|
path: &Path,
|
||||||
|
alias: &str,
|
||||||
|
home: &Path,
|
||||||
|
use_identity: bool,
|
||||||
|
connection: &mut SshConnection,
|
||||||
|
) -> Result<(), GitError> {
|
||||||
|
let Ok(contents) = fs::read_to_string(path) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let mut active = true;
|
||||||
|
let mut hostname_set = false;
|
||||||
|
let mut user_set = connection.user.is_some();
|
||||||
|
let mut port_set = connection.port != 22;
|
||||||
|
let mut identity_set = !use_identity;
|
||||||
|
for line in contents.lines() {
|
||||||
|
let Some(words) = shlex::split(line.split('#').next().unwrap_or_default()) else {
|
||||||
|
return Err(GitError::SshAuthenticationNotConfigured);
|
||||||
|
};
|
||||||
|
let Some((keyword, values)) = words.split_first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if keyword.eq_ignore_ascii_case("host") {
|
||||||
|
active = host_patterns_match(values, alias);
|
||||||
|
} else if active && keyword.eq_ignore_ascii_case("hostname") && !hostname_set {
|
||||||
|
if let Some(value) = values.first() {
|
||||||
|
connection.host = value.clone();
|
||||||
|
hostname_set = true;
|
||||||
|
}
|
||||||
|
} else if active && keyword.eq_ignore_ascii_case("user") && !user_set {
|
||||||
|
if let Some(value) = values.first() {
|
||||||
|
connection.user = Some(value.clone());
|
||||||
|
user_set = true;
|
||||||
|
}
|
||||||
|
} else if active && keyword.eq_ignore_ascii_case("port") && !port_set {
|
||||||
|
if let Some(value) = values.first() {
|
||||||
|
connection.port = value
|
||||||
|
.parse()
|
||||||
|
.ok()
|
||||||
|
.filter(|port| *port != 0)
|
||||||
|
.ok_or(GitError::SshAuthenticationNotConfigured)?;
|
||||||
|
port_set = true;
|
||||||
|
}
|
||||||
|
} else if active
|
||||||
|
&& keyword.eq_ignore_ascii_case("identityfile")
|
||||||
|
&& !identity_set
|
||||||
|
&& let Some(value) = values.first()
|
||||||
|
{
|
||||||
|
let expanded = value
|
||||||
|
.replace("%h", &connection.host)
|
||||||
|
.replace("%p", &connection.port.to_string())
|
||||||
|
.replace("%r", connection.user.as_deref().unwrap_or("git"));
|
||||||
|
let identity = expanded
|
||||||
|
.strip_prefix("~/")
|
||||||
|
.map_or_else(|| PathBuf::from(&expanded), |path| home.join(path));
|
||||||
|
connection.identity = SshIdentitySource::KeyFile(identity);
|
||||||
|
identity_set = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_patterns_match(patterns: &[String], host: &str) -> bool {
|
||||||
|
let mut matched = false;
|
||||||
|
for pattern in patterns {
|
||||||
|
let (negated, pattern) = pattern
|
||||||
|
.strip_prefix('!')
|
||||||
|
.map_or((false, pattern.as_str()), |pattern| (true, pattern));
|
||||||
|
if wildcard_match(pattern.as_bytes(), host.as_bytes()) {
|
||||||
|
if negated {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
matched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matched
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wildcard_match(pattern: &[u8], value: &[u8]) -> bool {
|
||||||
|
let (mut pattern_index, mut value_index, mut star, mut retry) = (0, 0, None, 0);
|
||||||
|
while value_index < value.len() {
|
||||||
|
if pattern
|
||||||
|
.get(pattern_index)
|
||||||
|
.is_some_and(|byte| *byte == b'?' || byte.eq_ignore_ascii_case(&value[value_index]))
|
||||||
|
{
|
||||||
|
pattern_index += 1;
|
||||||
|
value_index += 1;
|
||||||
|
} else if pattern.get(pattern_index) == Some(&b'*') {
|
||||||
|
star = Some(pattern_index);
|
||||||
|
pattern_index += 1;
|
||||||
|
retry = value_index;
|
||||||
|
} else if let Some(star_index) = star {
|
||||||
|
pattern_index = star_index + 1;
|
||||||
|
retry += 1;
|
||||||
|
value_index = retry;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pattern[pattern_index..].iter().all(|byte| *byte == b'*')
|
||||||
|
}
|
||||||
|
|
||||||
fn client_config() -> client::Config {
|
fn client_config() -> client::Config {
|
||||||
client::Config {
|
client::Config {
|
||||||
inactivity_timeout: Some(CONNECTION_TIMEOUT),
|
inactivity_timeout: Some(CONNECTION_TIMEOUT),
|
||||||
@@ -1034,6 +1179,39 @@ mod tests {
|
|||||||
server,
|
server,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn openssh_host_alias_resolves_connection_and_identity() {
|
||||||
|
let temporary = tempfile::tempdir().expect("temporary directory");
|
||||||
|
let config = temporary.path().join("config");
|
||||||
|
fs::write(
|
||||||
|
&config,
|
||||||
|
"Host *\n IdentityFile ~/.ssh/id_ed25519\nHost git.example\n HostName internal.example\n User git\n Port 2222\n",
|
||||||
|
)
|
||||||
|
.expect("SSH config");
|
||||||
|
let mut connection = super::SshConnection {
|
||||||
|
host: "git.example".to_owned(),
|
||||||
|
port: 22,
|
||||||
|
user: None,
|
||||||
|
identity: crate::config::SshIdentitySource::KeyFile(temporary.path().join("unused")),
|
||||||
|
known_hosts: temporary.path().join("known_hosts"),
|
||||||
|
};
|
||||||
|
super::apply_openssh_config(
|
||||||
|
&config,
|
||||||
|
"git.example",
|
||||||
|
temporary.path(),
|
||||||
|
true,
|
||||||
|
&mut connection,
|
||||||
|
)
|
||||||
|
.expect("resolve SSH config");
|
||||||
|
assert_eq!(connection.host, "internal.example");
|
||||||
|
assert_eq!(connection.user.as_deref(), Some("git"));
|
||||||
|
assert_eq!(connection.port, 2222);
|
||||||
|
assert_eq!(
|
||||||
|
connection.identity.key_file(),
|
||||||
|
Some(temporary.path().join(".ssh/id_ed25519").as_path())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
config::{GitRemote, RemoteEndpoint, SshFingerprint, SshRemoteAuthentication},
|
config::{GitRemote, RemoteEndpoint, SshFingerprint, SshRemoteAuthentication},
|
||||||
git::{GitError, GitOperationControl, SshPassphraseProvider},
|
git::{GitError, GitOperationControl, SshPassphraseProvider},
|
||||||
|
|||||||
@@ -102,6 +102,37 @@ fn explicit_relative_configuration_resolves_deterministically() -> TestResult {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "ios", target_os = "watchos")))]
|
||||||
|
#[test]
|
||||||
|
fn desktop_configuration_uses_repository_remotes_without_duplicate_settings() -> TestResult {
|
||||||
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
let vault = fixture.temporary.path().join("cwd/vault");
|
||||||
|
fs::create_dir_all(vault.join(".git"))?;
|
||||||
|
fs::write(
|
||||||
|
vault.join(".git/config"),
|
||||||
|
"[remote \"origin\"]\n\turl = git@git.example:team/store.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n",
|
||||||
|
)?;
|
||||||
|
fixture.write_explicit(
|
||||||
|
r#"
|
||||||
|
vault = "../vault"
|
||||||
|
default_key = "0123456789ABCDEF0123456789ABCDEF01234567"
|
||||||
|
key_material = "keys"
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let config = fixture
|
||||||
|
.loader()
|
||||||
|
.load(Some(Path::new("config/config.toml")))?;
|
||||||
|
assert_eq!(config.git_remotes().len(), 1);
|
||||||
|
assert_eq!(config.git_remotes()[0].name().as_str(), "origin");
|
||||||
|
assert_eq!(
|
||||||
|
config.git_remotes()[0].url(),
|
||||||
|
"git@git.example:team/store.git"
|
||||||
|
);
|
||||||
|
assert!(config.git_remotes()[0].ssh_authentication().is_none());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn git_commit_identity_defaults_validates_and_persists() -> TestResult {
|
fn git_commit_identity_defaults_validates_and_persists() -> TestResult {
|
||||||
let fixture = ConfigurationFixture::new()?;
|
let fixture = ConfigurationFixture::new()?;
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ ssh_agent_socket = "/run/user/1000/ssh-agent.socket"
|
|||||||
```
|
```
|
||||||
|
|
||||||
Private-key passphrases are stored by fingerprint in the operating-system
|
Private-key passphrases are stored by fingerprint in the operating-system
|
||||||
secret store and never appear in TOML. IronStorage does not read OpenSSH
|
secret store and never appear in TOML. Desktop builds obtain remotes from the
|
||||||
configuration, try additional keys, prompt for passwords or
|
password-store repository and honor the connection-only `Host`, `HostName`,
|
||||||
|
`User`, `Port`, and `IdentityFile` directives in `~/.ssh/config`. IronStorage
|
||||||
|
does not try additional keys, prompt for passwords or
|
||||||
keyboard-interactive authentication, launch an agent, or invoke proxy/helper
|
keyboard-interactive authentication, launch an agent, or invoke proxy/helper
|
||||||
commands. The normal CLI, TUI, and desktop builds enable the storage `ssh`
|
commands. The normal CLI, TUI, and desktop builds enable the storage `ssh`
|
||||||
feature, so the same configuration drives
|
feature, so the same configuration drives
|
||||||
|
|||||||
@@ -50,13 +50,16 @@ are read with size and line bounds; exact, hashed, and bracketed non-default
|
|||||||
port entries are supported. Unknown keys require an explicit call to persist
|
port entries are supported. Unknown keys require an explicit call to persist
|
||||||
the confirmed key atomically. Changed keys always fail and are never replaced.
|
the confirmed key atomically. Changed keys always fail and are never replaced.
|
||||||
|
|
||||||
Authentication uses one configured OpenSSH private-key file (Ed25519, ECDSA,
|
Desktop applications take remote names and URLs from the password-store's
|
||||||
or RSA) or one exact SHA-256 fingerprint from an already-running SSH agent.
|
`.git/config`. Authentication uses the matching `~/.ssh/config` identity or an
|
||||||
|
explicit OpenSSH private-key file (Ed25519, ECDSA, or RSA), or one exact SHA-256
|
||||||
|
fingerprint from an already-running SSH agent.
|
||||||
Encrypted-key passphrases come from `SecretBytes` in the operating-system
|
Encrypted-key passphrases come from `SecretBytes` in the operating-system
|
||||||
secret store. Identity attempts are bounded and deterministic; IronStorage
|
secret store. Identity attempts are bounded and deterministic; IronStorage
|
||||||
does not spray keys, use passwords or keyboard-interactive authentication,
|
does not spray keys, use passwords or keyboard-interactive authentication,
|
||||||
read OpenSSH configuration, start/probe an agent process, or run proxy/helper
|
start/probe an agent process or run proxy/helper commands. The in-process
|
||||||
commands. The explicit client allowlist excludes SHA-1 key exchange/MAC,
|
client reads `Host`, `HostName`, `User`, `Port`, and `IdentityFile`; executable
|
||||||
|
OpenSSH directives remain disabled. The explicit client allowlist excludes SHA-1 key exchange/MAC,
|
||||||
RSA/SHA-1 signatures, DSA, CBC, `none`, compression, and host certificates.
|
RSA/SHA-1 signatures, DSA, CBC, `none`, compression, and host certificates.
|
||||||
Cancellation interrupts connection and authentication without changing Git,
|
Cancellation interrupts connection and authentication without changing Git,
|
||||||
known hosts, or secure storage.
|
known hosts, or secure storage.
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ host/key interaction design.
|
|||||||
|
|
||||||
## Deliberate OpenSSH differences
|
## Deliberate OpenSSH differences
|
||||||
|
|
||||||
IronStorage does not read arbitrary OpenSSH configuration, `ProxyCommand`,
|
IronStorage reads the connection-only `Host`, `HostName`, `User`, `Port`, and
|
||||||
`Match`, URL rewrites, separate push URLs, host certificates, password or
|
`IdentityFile` directives from OpenSSH configuration. It does not support
|
||||||
|
`ProxyCommand`, `Match`, URL rewrites, separate push URLs, host certificates, password or
|
||||||
keyboard-interactive authentication, agent forwarding, arbitrary remote
|
keyboard-interactive authentication, agent forwarding, arbitrary remote
|
||||||
commands, local transport helpers, or local-path remotes. These are explicit
|
commands, local transport helpers, or local-path remotes. These are explicit
|
||||||
security boundaries, not partial implementations. HTTPS smart Git remains the
|
security boundaries, not partial implementations. HTTPS smart Git remains the
|
||||||
|
|||||||
Reference in New Issue
Block a user