Implement SSH identity authentication
This commit is contained in:
@@ -701,7 +701,7 @@ url = "https://example.test/team/store.git"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_remote_configuration_round_trips_without_https_credentials() -> TestResult {
|
||||
fn ssh_remote_configuration_round_trips_with_secret_free_authentication() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
@@ -712,12 +712,27 @@ key_material = "keys"
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "git@example.test:team/store.git"
|
||||
ssh_identity_file = "keys/id_ed25519"
|
||||
ssh_known_hosts_file = "known_hosts"
|
||||
"#,
|
||||
)?;
|
||||
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
let remote = &config.git_remotes()[0];
|
||||
assert_eq!(remote.url(), "git@example.test:team/store.git");
|
||||
assert!(remote.https_credentials().is_none());
|
||||
let authentication = remote.ssh_authentication().expect("SSH authentication");
|
||||
assert_eq!(
|
||||
authentication.identity().key_file(),
|
||||
Some(
|
||||
fs::canonicalize(fixture.temporary.path())?
|
||||
.join("cwd/config/keys/id_ed25519")
|
||||
.as_path()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
authentication.known_hosts_file(),
|
||||
fs::canonicalize(fixture.temporary.path())?.join("cwd/config/known_hosts")
|
||||
);
|
||||
|
||||
config.update_git_identity(&GitIdentity::new("Alice", "alice@example.test")?)?;
|
||||
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
@@ -725,6 +740,70 @@ url = "git@example.test:team/store.git"
|
||||
let persisted = fs::read_to_string(fixture.explicit_path())?;
|
||||
assert!(!persisted.contains("server_id"));
|
||||
assert!(!persisted.contains("application_id"));
|
||||
assert!(!persisted.contains("passphrase"));
|
||||
assert!(persisted.contains("ssh_identity_file"));
|
||||
assert!(persisted.contains("ssh_known_hosts_file"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_authentication_requires_exactly_one_identity_source() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "ssh://git@example.test/team/store.git"
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(&fixture.explicit_path()))
|
||||
.expect_err("SSH identity is required"),
|
||||
ConfigError::InvalidField {
|
||||
field: "git.remotes.ssh_authentication"
|
||||
}
|
||||
);
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "ssh://git@example.test/team/store.git"
|
||||
ssh_agent_fingerprint = "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
ssh_agent_socket = "agent.sock"
|
||||
ssh_known_hosts_file = "known_hosts"
|
||||
"#,
|
||||
)?;
|
||||
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
let authentication = config.git_remotes()[0]
|
||||
.ssh_authentication()
|
||||
.expect("SSH authentication");
|
||||
assert_eq!(
|
||||
authentication
|
||||
.identity()
|
||||
.agent_fingerprint()
|
||||
.expect("agent fingerprint")
|
||||
.as_str(),
|
||||
"SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
||||
);
|
||||
assert_eq!(
|
||||
authentication.identity().agent_socket(),
|
||||
Some(
|
||||
fs::canonicalize(fixture.temporary.path())?
|
||||
.join("cwd/config/agent.sock")
|
||||
.as_path()
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ use std::{
|
||||
};
|
||||
|
||||
use ironstorage::{
|
||||
config::{ConfigLoader, GitRemote},
|
||||
config::{ConfigLoader, GitRemote, SshFingerprint},
|
||||
crypto::{CryptoError, KeyInfo, KeyStore, SecretProvider as _, SecretProviderError},
|
||||
git::{GitCredentialProvider as _, GitError},
|
||||
git::{GitCredentialProvider as _, GitError, SshPassphraseProvider as _},
|
||||
repository::{EncryptedEntry, SecretBytes},
|
||||
secret_store::{
|
||||
OpenPgpPassphrasePrompt, OpenPgpPassphrasePromptError, SecretCachePolicy, SecretLocator,
|
||||
@@ -274,6 +274,41 @@ fn denied_cancelled_unavailable_and_corrupted_are_typed_and_redacted() -> TestRe
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_passphrases_are_retrieved_by_fingerprint_with_typed_access_failures() -> TestResult {
|
||||
let backend = MemoryBackend::default();
|
||||
let store = store(backend.clone());
|
||||
store.unlock()?;
|
||||
let fingerprint = SshFingerprint::parse("SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")?;
|
||||
let reference = SecretReference::ssh_key_passphrase(fingerprint.clone());
|
||||
store.create(
|
||||
&reference,
|
||||
SecretBytes::new(b"protected-passphrase".to_vec()),
|
||||
)?;
|
||||
assert_eq!(
|
||||
store.ssh_key_passphrase(&fingerprint)?.expose(),
|
||||
b"protected-passphrase"
|
||||
);
|
||||
backend.fail_next(SecretStoreError::Denied);
|
||||
assert!(matches!(
|
||||
store.ssh_key_passphrase(&fingerprint),
|
||||
Err(GitError::SshKeyPassphraseDenied { fingerprint: denied }) if denied == fingerprint
|
||||
));
|
||||
backend.fail_next(SecretStoreError::Cancelled);
|
||||
assert!(matches!(
|
||||
store.ssh_key_passphrase(&fingerprint),
|
||||
Err(GitError::SshKeyPassphraseCancelled { fingerprint: cancelled })
|
||||
if cancelled == fingerprint
|
||||
));
|
||||
let missing = SshFingerprint::parse("SHA256:AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE")?;
|
||||
assert!(matches!(
|
||||
store.ssh_key_passphrase(&missing),
|
||||
Err(GitError::SshKeyPassphraseUnavailable { fingerprint }) if fingerprint == missing
|
||||
));
|
||||
assert!(!format!("{reference:?}").contains("protected-passphrase"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_cache_is_cleared_by_lock_and_never_aliases_git_accounts() -> TestResult {
|
||||
let backend = MemoryBackend::default();
|
||||
|
||||
Reference in New Issue
Block a user