From 4af324a6ef091974823f372597b48ad2465e66ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Kj=C3=A4ll?= Date: Wed, 4 Sep 2024 20:48:39 +0200 Subject: [PATCH] no need to verify host keys as that is now part of the git2 library --- Cargo.lock | 297 +++++++++++++++++++++++++++-------- Cargo.toml | 8 +- src/git.rs | 393 +---------------------------------------------- src/tests/git.rs | 16 +- 4 files changed, 241 insertions(+), 473 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 062c942..1179a49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,6 +112,12 @@ dependencies = [ "term", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -139,12 +145,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -336,9 +336,9 @@ dependencies = [ [[package]] name = "capnp-rpc" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b499a69c75f961512a596310fa46f18a9ad287fc75d50546ac6536f7428c803" +checksum = "04f09758ec79a2825c9bc87cbe1c8ded306c99a83b52cf89577667ebff2d3ff7" dependencies = [ "capnp", "capnp-futures", @@ -362,9 +362,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.15" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" dependencies = [ "jobserver", "libc", @@ -1666,15 +1666,15 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", "http", "indexmap", "slab", @@ -1743,9 +1743,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1754,12 +1754,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", "pin-project-lite", ] @@ -1769,47 +1781,77 @@ version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - [[package]] name = "hyper" -version = "0.14.30" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", "http", "http-body", "httparse", - "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", + "http-body-util", "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -2828,6 +2870,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -3045,20 +3107,24 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", "http", "http-body", + "http-body-util", "hyper", + "hyper-rustls", "hyper-tls", + "hyper-util", "ipnet", "js-sys", "log", @@ -3080,7 +3146,22 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.50.0", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", ] [[package]] @@ -3089,7 +3170,6 @@ version = "0.7.0-alpha" dependencies = [ "anyhow", "arboard", - "base64 0.22.1", "chrono", "config", "criterion", @@ -3098,14 +3178,11 @@ dependencies = [ "glob", "gpgme", "hex", - "hmac", "rand", "reqwest", "sequoia-gpg-agent", "sequoia-ipc", "sequoia-openpgp", - "sha1", - "sha2", "tar", "tempfile", "toml 0.8.19", @@ -3197,13 +3274,44 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ - "base64 0.21.7", + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -3331,7 +3439,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13261ee216b44d932ef93b2d4a75d45199bef77864bcc5b77ecfc7bc0ecb02d6" dependencies = [ "anyhow", - "base64 0.22.1", + "base64", "buffered-reader", "bzip2", "chrono", @@ -3529,6 +3637,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3579,9 +3693,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -3596,20 +3713,20 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3812,6 +3929,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -3884,6 +4012,27 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3992,6 +4141,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.2" @@ -4277,6 +4432,36 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -4443,16 +4628,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 5a95c17..23e7875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "GPL-3.0-only" edition = '2021' [dependencies] -arboard = "3.3.2" +arboard = "3.4.0" glob = "0.3.1" gpgme = "0.11.0" chrono = { version = "0.4", default-features = false, features = ["clock"] } @@ -17,17 +17,13 @@ git2 = "0.19.0" rand = "0.8.5" whoami = "1.4.1" toml = "0.8.10" -reqwest = { version = "0.11.25", features = ["blocking"] } +reqwest = { version = "0.12", features = ["blocking"] } hex = "0.4.3" totp-rs = { version = "5.5.1", features = ["otpauth"] } sequoia-openpgp = "1.21.0" anyhow = "1.0.80" sequoia-ipc = "0.35.0" sequoia-gpg-agent = "0.4.0" -base64 = "0.22.0" -sha2 = "0.10.8" -sha1 = "0.10.6" -hmac = "0.12.1" zeroize = { version = "1.8.0", features = ["zeroize_derive", "alloc"] } [dependencies.config] diff --git a/src/git.rs b/src/git.rs index d2e8fdc..2fa6657 100644 --- a/src/git.rs +++ b/src/git.rs @@ -1,17 +1,11 @@ use std::{ - fmt::{Display, Write}, + fmt::Display, path::{Path, PathBuf}, str, }; -use base64::{ - engine::general_purpose::{STANDARD, STANDARD_NO_PAD}, - Engine, -}; use chrono::{DateTime, Local, TimeZone}; -use git2::{cert::Cert, CertificateCheckStatus, Oid, Repository}; -use hmac::Mac; -use sha2::{Digest, Sha256}; +use git2::{Oid, Repository}; use crate::{ crypto::{Crypto, FindSigningFingerprintStrategy, VerificationError}, @@ -20,8 +14,6 @@ use crate::{ signature::SignatureStatus, }; -const HASH_HOSTNAME_PREFIX: &str = "|1|"; - fn git_branch_name(repo: &git2::Repository) -> Result { let head = repo.find_reference("HEAD")?; let symbolic = head @@ -275,8 +267,6 @@ pub fn push(store: &PasswordStore) -> Result<()> { callbacks.credentials(|_url, username, allowed| { cred(&mut tried_ssh_key, _url, username, allowed) }); - callbacks - .certificate_check(|cert, host| certificate_check(cert, host, &store.get_user_home())); callbacks.push_update_reference(|_refname, status| { ref_status = status.map(std::borrow::ToOwned::to_owned); Ok(()) @@ -308,8 +298,6 @@ pub fn pull(store: &PasswordStore) -> Result<()> { let mut tried_ssh_key = false; cb.credentials(|_url, username, allowed| cred(&mut tried_ssh_key, _url, username, allowed)); - cb.certificate_check(|cert, host| certificate_check(cert, host, &store.get_user_home())); - let mut opts = git2::FetchOptions::new(); opts.remote_callbacks(cb); origin.fetch(&[branch_name], Some(&mut opts), None)?; @@ -479,383 +467,6 @@ fn name_from_commit(commit: &git2::Commit) -> Result { }) } -/// The git2 callback used to validate a certificate (only ssh known hosts are validated). -pub fn certificate_check( - cert: &Cert<'_>, - host: &str, - home: &Option, -) -> std::result::Result { - let port: Option = Some(22); - let Some(host_key) = cert.as_hostkey() else { - // Return passthrough for TLS X509 certificates to use whatever validation - // was done in git2. - return Ok(CertificateCheckStatus::CertificatePassthrough); - }; - // If a nonstandard port is in use, check for that first. - // The fallback to check without a port is handled in the HostKeyNotFound handler. - let host_maybe_port = match port { - Some(port) if port != 22 => format!("[{host}]:{port}"), - _ => host.to_string(), - }; - // The error message must be constructed as a string to pass through the libgit2 C API. - let err_msg = match check_ssh_known_hosts(host_key, &host_maybe_port, home) { - Ok(()) => { - return Ok(CertificateCheckStatus::CertificateOk); - } - Err(KnownHostError::CheckError(e)) => { - format!("error: failed to validate host key:\n{e:#}") - } - Err(KnownHostError::HostKeyNotFound { - hostname, - key_type, - remote_host_key, - remote_fingerprint, - other_hosts, - }) => { - // Try checking without the port. - if port.is_some() - && !matches!(port, Some(22)) - && check_ssh_known_hosts(host_key, host, home).is_ok() - { - return Ok(CertificateCheckStatus::CertificateOk); - } - let key_type_short_name = key_type.short_name(); - let key_type_name = key_type.name(); - let known_hosts_location = user_known_host_location_to_add(home); - let other_hosts_message = if other_hosts.is_empty() { - String::new() - } else { - let mut msg = String::from( - "Note: This host key was found, \ - but is associated with a different host:\n", - ); - for known_host in other_hosts { - let loc = match known_host.location { - KnownHostLocation::File { path, lineno } => { - format!("{} line {lineno}", path.display()) - } - }; - writeln!(msg, " {loc}: {}", known_host.patterns).unwrap(); - } - msg - }; - format!("error: unknown SSH host key\n\ - The SSH host key for `{hostname}` is not known and cannot be validated.\n\ - \n\ - To resolve this issue, add the host key to {known_hosts_location}\n\ - \n\ - The key to add is:\n\ - \n\ - {hostname} {key_type_name} {remote_host_key}\n\ - \n\ - The {key_type_short_name} key fingerprint is: SHA256:{remote_fingerprint}\n\ - This fingerprint should be validated with the server administrator that it is correct.\n\ - {other_hosts_message}\n\ - See https://doc.rust-lang.org/nightly/cargo/appendix/git-authentication.html#ssh-known-hosts \ - for more information.\n\ - ") - } - Err(KnownHostError::HostKeyHasChanged { - hostname, - key_type, - old_known_host, - remote_host_key, - remote_fingerprint, - }) => { - let key_type_short_name = key_type.short_name(); - let key_type_name = key_type.name(); - let known_hosts_location = user_known_host_location_to_add(home); - let old_key_resolution = match old_known_host.location { - KnownHostLocation::File { path, lineno } => { - let old_key_location = path.display(); - format!( - "removing the old {key_type_name} key for `{hostname}` \ - located at {old_key_location} line {lineno}, \ - and adding the new key to {known_hosts_location}", - ) - } - }; - format!("error: SSH host key has changed for `{hostname}`\n\ - *********************************\n\ - * WARNING: HOST KEY HAS CHANGED *\n\ - *********************************\n\ - This may be caused by a man-in-the-middle attack, or the \ - server may have changed its host key.\n\ - \n\ - The {key_type_short_name} fingerprint for the key from the remote host is:\n\ - SHA256:{remote_fingerprint}\n\ - \n\ - You are strongly encouraged to contact the server \ - administrator for `{hostname}` to verify that this new key is \ - correct.\n\ - \n\ - If you can verify that the server has a new key, you can \ - resolve this error by {old_key_resolution}\n\ - \n\ - The key provided by the remote host is:\n\ - \n\ - {hostname} {key_type_name} {remote_host_key}\n\ - \n\ - See https://doc.rust-lang.org/nightly/cargo/appendix/git-authentication.html#ssh-known-hosts \ - for more information.\n\ - ") - } - }; - Err(git2::Error::new( - git2::ErrorCode::GenericError, - git2::ErrorClass::Callback, - err_msg, - )) -} - -enum KnownHostError { - /// Some general error happened while validating the known hosts. - CheckError(anyhow::Error), - /// The host key was not found. - HostKeyNotFound { - hostname: String, - key_type: git2::cert::SshHostKeyType, - remote_host_key: String, - remote_fingerprint: String, - other_hosts: Vec, - }, - /// The host key was found, but does not match the remote's key. - HostKeyHasChanged { - hostname: String, - key_type: git2::cert::SshHostKeyType, - old_known_host: KnownHost, - remote_host_key: String, - remote_fingerprint: String, - }, -} - -impl From for KnownHostError { - fn from(err: anyhow::Error) -> KnownHostError { - KnownHostError::CheckError(err) - } -} - -/// The location where a host key was located. -#[derive(Clone)] -enum KnownHostLocation { - /// Loaded from a file from disk. - File { path: PathBuf, lineno: u32 }, -} - -/// A single known host entry. -#[derive(Clone)] -struct KnownHost { - location: KnownHostLocation, - /// The hostname. May be comma separated to match multiple hosts. - patterns: String, - key_type: String, - key: Vec, -} - -impl KnownHost { - /// Returns whether or not the given host matches this known host entry. - fn host_matches(&self, host: &str) -> bool { - let mut match_found = false; - let host = host.to_lowercase(); - if let Some(hashed) = self.patterns.strip_prefix(HASH_HOSTNAME_PREFIX) { - return hashed_hostname_matches(&host, hashed); - } - for pattern in self.patterns.split(',') { - let pattern = pattern.to_lowercase(); - // FIXME: support * and ? wildcards - if let Some(pattern) = pattern.strip_prefix('!') { - if pattern == host { - return false; - } - } else { - match_found = pattern == host; - } - } - match_found - } -} - -fn hashed_hostname_matches(host: &str, hashed: &str) -> bool { - let Some((b64_salt, b64_host)) = hashed.split_once('|') else { - return false; - }; - let Ok(salt) = STANDARD.decode(b64_salt) else { - return false; - }; - let Ok(hashed_host) = STANDARD.decode(b64_host) else { - return false; - }; - let Ok(mut mac) = hmac::Hmac::::new_from_slice(&salt) else { - return false; - }; - mac.update(host.as_bytes()); - let result = mac.finalize().into_bytes(); - hashed_host == result[..] -} - -/// Checks if the given host/host key pair is known. -#[allow(clippy::result_large_err)] -fn check_ssh_known_hosts( - cert_host_key: &git2::cert::CertHostkey<'_>, - host: &str, - home: &Option, -) -> std::result::Result<(), KnownHostError> { - let Some(remote_host_key) = cert_host_key.hostkey() else { - return Err(anyhow::format_err!("remote host key is not available").into()); - }; - let remote_key_type = cert_host_key.hostkey_type().unwrap(); - // `changed_key` keeps track of any entries where the key has changed. - let mut changed_key = None; - // `other_hosts` keeps track of any entries that have an identical key, - // but a different hostname. - let mut other_hosts = Vec::new(); - - // Collect all the known host entries from disk. - let mut known_hosts = Vec::new(); - for path in known_host_files(home) { - if !path.exists() { - continue; - } - let hosts = load_hostfile(&path)?; - known_hosts.extend(hosts); - } - - for known_host in known_hosts { - // The key type from libgit2 needs to match the key type from the host file. - if known_host.key_type != remote_key_type.name() { - continue; - } - let key_matches = known_host.key == remote_host_key; - if !known_host.host_matches(host) { - // `name` can be None for hashed hostnames (which libgit2 does not expose). - if key_matches { - other_hosts.push(known_host.clone()); - } - continue; - } - if key_matches { - return Ok(()); - } - // The host and key type matched, but the key itself did not. - // This indicates the key has changed. - // This is only reported as an error if no subsequent lines have a - // correct key. - changed_key = Some(known_host.clone()); - } - // Older versions of OpenSSH (before 6.8, March 2015) showed MD5 - // fingerprints (see FingerprintHash ssh config option). Here we only - // support SHA256. - let remote_fingerprint = { - let mut hasher = Sha256::new(); - hasher.update(remote_host_key); - hasher.finalize() - }; - let remote_fingerprint = STANDARD_NO_PAD.encode(remote_fingerprint); - let remote_host_key = STANDARD.encode(remote_host_key); - // FIXME: Ideally the error message should include the IP address of the - // remote host (to help the user validate that they are connecting to the - // host they were expecting to). However, I don't see a way to obtain that - // information from libgit2. - match changed_key { - Some(old_known_host) => Err(KnownHostError::HostKeyHasChanged { - hostname: host.to_string(), - key_type: remote_key_type, - old_known_host, - remote_host_key, - remote_fingerprint, - }), - None => Err(KnownHostError::HostKeyNotFound { - hostname: host.to_string(), - key_type: remote_key_type, - remote_host_key, - remote_fingerprint, - other_hosts, - }), - } -} - -/// Returns a list of files to try loading OpenSSH-formatted known hosts. -fn known_host_files(home: &Option) -> Vec { - let mut result = Vec::new(); - result.push(PathBuf::from("/etc/ssh/ssh_known_hosts")); - result.extend(user_known_host_location(home)); - result -} - -/// The location to display in an error message instructing the user where to -/// add the new key. -fn user_known_host_location_to_add(home: &Option) -> String { - // Note that we don't bother with the legacy known_hosts2 files. - match user_known_host_location(home) { - Some(path) => path.to_str().expect("utf-8 home").to_string(), - None => "~/.ssh/known_hosts".to_string(), - } -} - -/// The location of the user's known_hosts file. -fn user_known_host_location(home: &Option) -> Option { - // NOTE: This is a potentially inaccurate prediction of what the user - // actually wants. The actual location depends on several factors: - // - // - Windows OpenSSH Powershell version: I believe this looks up the home - // directory via ProfileImagePath in the registry, falling back to - // `GetWindowsDirectoryW` if that fails. - // - OpenSSH Portable (under msys): This is very complicated. I got lost - // after following it through some ldap/active directory stuff. - // - OpenSSH (most unix platforms): Uses `pw->pw_dir` from `getpwuid()`. - // - // This doesn't do anything close to that. home_dir's behavior is: - // - Windows: $USERPROFILE, or SHGetFolderPathW() - // - Unix: $HOME, or getpwuid_r() - // - // Since there is a mismatch here, the location returned here might be - // different than what the user's `ssh` CLI command uses. We may want to - // consider trying to align it better. - home.clone().map(|mut home| { - home.push(".ssh"); - home.push("known_hosts"); - home - }) -} - -/// Loads an OpenSSH known_hosts file. -fn load_hostfile(path: &Path) -> std::result::Result, anyhow::Error> { - let contents = String::from_utf8(std::fs::read(path)?)?; - let entries = contents - .lines() - .enumerate() - .filter_map(|(lineno, line)| { - let location = KnownHostLocation::File { - path: path.to_path_buf(), - lineno: lineno as u32 + 1, - }; - parse_known_hosts_line(line, location) - }) - .collect(); - Ok(entries) -} - -fn parse_known_hosts_line(line: &str, location: KnownHostLocation) -> Option { - let line = line.trim(); - // FIXME: @revoked and @cert-authority is currently not supported. - if line.is_empty() || line.starts_with(['#', '@']) { - return None; - } - let mut parts = line.split([' ', '\t']).filter(|s| !s.is_empty()); - let patterns = parts.next()?; - let key_type = parts.next()?; - let key = parts.next()?; - let Ok(key) = STANDARD.decode(key) else { - return None; - }; - Some(KnownHost { - location, - patterns: patterns.to_string(), - key_type: key_type.to_string(), - key, - }) -} - #[cfg(test)] #[path = "tests/git.rs"] mod git_tests; diff --git a/src/tests/git.rs b/src/tests/git.rs index a6c540a..da3a22c 100644 --- a/src/tests/git.rs +++ b/src/tests/git.rs @@ -1,18 +1,4 @@ -use crate::{ - error::Result, - git::{hashed_hostname_matches, should_sign}, - test_helpers::UnpackedDir, -}; - -#[test] -fn hashed_hostname_matches_github_com() { - let result = hashed_hostname_matches( - "github.com", - "QI6BGIOtEYviGBfiW2nsZ+JxeAY=|PnXH1BrfyPNBQ1fcKZmCeA7feLc=", - ); - - assert!(result); -} +use crate::{error::Result, git::should_sign, test_helpers::UnpackedDir}; #[test] fn test_should_sign_true() -> Result<()> {