Skip to content

Commit 2b13234

Browse files
LibGit2: patch to pass hostkey & port to host verify callback (#39324)
It seems that no one actually verifies SSH host identity with libgit2 because the callback doesn't give enough information do so correctly: - It doesn't give the actual host key fingerprint, but rather three different hashes thereof. This means we cannot distinguish a known hosts entry that has a different type (`ssh-rsa`, `ssh-dsa`, etc.) from an entry with a matching type and a fingerprint mismatch: the former should be treated as an unknown host whereas the latter is a host key mismatch; they cannot be distinguished without this patch. - If the user connects on a non-default port (i.e. not 22), this is not passed to the callback in any way. Since there can be different known host entries for different ports and they should be treated as distinct, this also means the current API cannot be used to verify hosts serving SSH on non-standard ports. This patch passes the port. I will try to upstream some version of this patch to libgit2. The same patch has already been applied to the LibGit2 JLL. Fixes #38777. Might fix JuliaLang/Pkg.jl#2334.
1 parent cf12e49 commit 2b13234

File tree

8 files changed

+158
-151
lines changed

8 files changed

+158
-151
lines changed

deps/Versions.make

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ CURL_JLL_NAME := LibCURL
3333
LAPACK_VER := 3.9.0
3434

3535
# LibGit2
36-
LIBGIT2_VER := 1.1.0
36+
LIBGIT2_JLL_VER := 1.2.1+0
3737
LIBGIT2_JLL_NAME := LibGit2
3838

3939
# LibSSH2
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
48b3eb5811566f1cc70a9581b8f702f4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
46af2fbe9c96a18a97531aefc79e710abd8e12eca64ddcb2a0ddc8bc675dbaed0723ddbd4401d870eddcae04d99c4306cc6bdaa54b063de36d7fc0981ba86587

deps/libgit2.mk

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,15 @@ $(LIBGIT2_SRC_PATH)/libgit2-mbedtls-incdir.patch-applied: $(LIBGIT2_SRC_PATH)/li
4646
patch -p1 -f < $(SRCDIR)/patches/libgit2-mbedtls-incdir.patch
4747
echo 1 > $@
4848

49+
$(LIBGIT2_SRC_PATH)/libgit2-hostkey.patch-applied: $(LIBGIT2_SRC_PATH)/libgit2-mbedtls-incdir.patch-applied
50+
cd $(LIBGIT2_SRC_PATH) && \
51+
patch -p1 -f < $(SRCDIR)/patches/libgit2-hostkey.patch
52+
echo 1 > $@
53+
4954
$(BUILDDIR)/$(LIBGIT2_SRC_DIR)/build-configured: \
5055
$(LIBGIT2_SRC_PATH)/libgit2-agent-nonfatal.patch-applied \
51-
$(LIBGIT2_SRC_PATH)/libgit2-mbedtls-incdir.patch-applied
56+
$(LIBGIT2_SRC_PATH)/libgit2-mbedtls-incdir.patch-applied \
57+
$(LIBGIT2_SRC_PATH)/libgit2-hostkey.patch-applied
5258

5359
$(BUILDDIR)/$(LIBGIT2_SRC_DIR)/build-configured: $(LIBGIT2_SRC_PATH)/source-extracted
5460
mkdir -p $(dir $@)

deps/patches/libgit2-hostkey.patch

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
diff --git a/include/git2/cert.h b/include/git2/cert.h
2+
index e8cd2d180..54293cd31 100644
3+
--- a/include/git2/cert.h
4+
+++ b/include/git2/cert.h
5+
@@ -111,6 +111,14 @@ typedef struct {
6+
* have the SHA-256 hash of the hostkey.
7+
*/
8+
unsigned char hash_sha256[32];
9+
+
10+
+ /**
11+
+ * Hostkey itself.
12+
+ */
13+
+ int hostkey_type;
14+
+ size_t hostkey_len;
15+
+ unsigned char hostkey[1024];
16+
+
17+
} git_cert_hostkey;
18+
19+
/**
20+
diff --git a/src/transports/ssh.c b/src/transports/ssh.c
21+
index f4ed05bb1..049697796 100644
22+
--- a/src/transports/ssh.c
23+
+++ b/src/transports/ssh.c
24+
@@ -523,6 +523,7 @@ static int _git_ssh_setup_conn(
25+
git_credential *cred = NULL;
26+
LIBSSH2_SESSION* session=NULL;
27+
LIBSSH2_CHANNEL* channel=NULL;
28+
+ char *host_and_port;
29+
30+
t->current_stream = NULL;
31+
32+
@@ -566,6 +567,12 @@ post_extract:
33+
34+
cert.parent.cert_type = GIT_CERT_HOSTKEY_LIBSSH2;
35+
36+
+ key = libssh2_session_hostkey(session, &cert.hostkey_len, &cert.hostkey_type);
37+
+ bzero(&cert.hostkey, sizeof(cert.hostkey));
38+
+ if (cert.hostkey_len > sizeof(cert.hostkey))
39+
+ cert.hostkey_len = sizeof(cert.hostkey);
40+
+ memcpy(&cert.hostkey, key, cert.hostkey_len);
41+
+
42+
#ifdef LIBSSH2_HOSTKEY_HASH_SHA256
43+
key = libssh2_hostkey_hash(session, LIBSSH2_HOSTKEY_HASH_SHA256);
44+
if (key != NULL) {
45+
@@ -597,7 +604,15 @@ post_extract:
46+
47+
cert_ptr = &cert;
48+
49+
- error = t->owner->certificate_check_cb((git_cert *) cert_ptr, 0, urldata.host, t->owner->message_cb_payload);
50+
+ if (git_net_url_is_default_port(&urldata)) {
51+
+ host_and_port = urldata.host;
52+
+ } else {
53+
+ size_t n = strlen(urldata.host) + strlen(urldata.port) + 2;
54+
+ host_and_port = alloca(n);
55+
+ sprintf(host_and_port, "%s:%s", urldata.host, urldata.port);
56+
+ }
57+
+
58+
+ error = t->owner->certificate_check_cb((git_cert *) cert_ptr, 0, host_and_port, t->owner->message_cb_payload);
59+
60+
if (error < 0 && error != GIT_PASSTHROUGH) {
61+
if (!git_error_last())

stdlib/LibGit2/src/callbacks.jl

Lines changed: 54 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -360,24 +360,14 @@ function fetchhead_foreach_callback(ref_name::Cstring, remote_url::Cstring,
360360
end
361361

362362
struct CertHostKey
363-
parent :: Cint
364-
mask :: Cint
365-
md5 :: NTuple{16,UInt8}
366-
sha1 :: NTuple{20,UInt8}
367-
sha256 :: NTuple{32,UInt8}
368-
end
369-
370-
struct KeyHashes
371-
sha1 :: Union{NTuple{20,UInt8}, Nothing}
372-
sha256 :: Union{NTuple{32,UInt8}, Nothing}
373-
end
374-
375-
function KeyHashes(cert_p::Ptr{CertHostKey})
376-
cert = unsafe_load(cert_p)
377-
return KeyHashes(
378-
cert.mask & Consts.CERT_SSH_SHA1 != 0 ? cert.sha1 : nothing,
379-
cert.mask & Consts.CERT_SSH_SHA256 != 0 ? cert.sha256 : nothing,
380-
)
363+
parent :: Cint
364+
mask :: Cint
365+
md5 :: NTuple{16,UInt8}
366+
sha1 :: NTuple{20,UInt8}
367+
sha256 :: NTuple{32,UInt8}
368+
type :: Cint
369+
len :: Csize_t
370+
data :: NTuple{1024,UInt8}
381371
end
382372

383373
function verify_host_error(message::AbstractString)
@@ -406,22 +396,21 @@ function certificate_callback(
406396
return Consts.CERT_REJECT
407397
elseif transport == "SSH"
408398
# SSH verification has to be done here
409-
files = [joinpath(homedir(), ".ssh", "known_hosts")]
410-
check = ssh_knownhost_check(files, host, KeyHashes(cert_p))
399+
files = NetworkOptions.ssh_known_hosts_files()
400+
cert = unsafe_load(cert_p)
401+
check = ssh_knownhost_check(files, host, cert)
411402
valid = false
412-
if check == Consts.SSH_HOST_KNOWN
403+
if check == Consts.LIBSSH2_KNOWNHOST_CHECK_MATCH
413404
valid = true
414-
elseif check == Consts.SSH_HOST_UNKNOWN
405+
elseif check == Consts.LIBSSH2_KNOWNHOST_CHECK_NOTFOUND
415406
if Sys.which("ssh-keyscan") !== nothing
416407
msg = "Please run `ssh-keyscan $host >> $(files[1])` in order to add the server to your known hosts file and then try again."
417408
else
418409
msg = "Please connect once using `ssh $host` in order to add the server to your known hosts file and then try again. You may not be allowed to log in (wrong user and/or no login allowed), but ssh will prompt you to add a host key for the server which will allow libgit2 to verify the server."
419410
end
420411
verify_host_error("SSH host verification: the server `$host` is not a known host. $msg")
421-
elseif check == Consts.SSH_HOST_MISMATCH
412+
elseif check == Consts.LIBSSH2_KNOWNHOST_CHECK_MISMATCH
422413
verify_host_error("SSH host verification: the identity of the server `$host` does not match its known hosts record. Someone could be trying to man-in-the-middle your connection. It is also possible that the server has changed its key, in which case you should check with the server administrator and if they confirm that the key has been changed, update your known hosts file.")
423-
elseif check == Consts.SSH_HOST_BAD_HASH
424-
verify_host_error("SSH host verification: no secure certificate hash available for `$host`, cannot verify server identity.")
425414
else
426415
@error("unexpected SSH known host check result", check)
427416
end
@@ -431,31 +420,6 @@ function certificate_callback(
431420
return Consts.CERT_REJECT
432421
end
433422

434-
## SSH known host checking
435-
#
436-
# We can't use libssh2_knownhost_check because libgit2, for no good reason,
437-
# doesn't give us a host fingerprint that we can use for that and instead gives
438-
# us multiple hashes of that fingerprint instead. Moreover, since a host can
439-
# have multiple fingerprints in the known hosts file with different encryption
440-
# types (gitlab.com does this, for example), we need to iterate through all the
441-
# known hosts entries and manually check if any of them is a match.
442-
#
443-
# The fact that libgit2 won't give us a fingerprint also means that we cannot,
444-
# even if we wanted to, prompt the user for whether to add the fingerprint to
445-
# the known hosts file, since we don't have the fingerprint that should be
446-
# added. The only option is to instruct the user how to add it themselves.
447-
#
448-
# Check logic: if a host appears in a known hosts file at all then one of the
449-
# keys in that file must match or we declare a mismatch; if the host name
450-
# doesn't appear in the file at all, however, we will continue searching files.
451-
#
452-
# This allows adding a host to the system known hosts file to fully override
453-
# that host appearing in a bundled known hosts file. It is necessary to allow
454-
# any of multiple entries in a single file to match, however, to allow for the
455-
# possiblity that the file contains multiple fingerprints for the same host. If
456-
# libgit2 gave us the fucking fingerprint then we could search for only an entry
457-
# with the correct type, but we can't do that without the actual fingerprint.
458-
459423
struct KnownHost
460424
magic :: Cuint
461425
node :: Ptr{Cvoid}
@@ -465,12 +429,27 @@ struct KnownHost
465429
end
466430

467431
function ssh_knownhost_check(
468-
files :: AbstractVector{<:AbstractString},
469-
host :: AbstractString,
470-
hashes :: KeyHashes,
432+
files :: AbstractVector{<:AbstractString},
433+
host :: AbstractString,
434+
cert :: CertHostKey,
435+
)
436+
key = collect(cert.data)[1:cert.len]
437+
return ssh_knownhost_check(files, host, key)
438+
end
439+
440+
function ssh_knownhost_check(
441+
files :: AbstractVector{<:AbstractString},
442+
host :: AbstractString,
443+
key :: String,
471444
)
472-
hashes.sha1 === hashes.sha256 === nothing &&
473-
return Consts.SSH_HOST_BAD_HASH
445+
if (m = match(r"^(.+):(\d+)$", host)) !== nothing
446+
host = m.captures[1]
447+
port = parse(Int, m.captures[2])
448+
else
449+
port = 22 # default SSH port
450+
end
451+
mask = Consts.LIBSSH2_KNOWNHOST_TYPE_PLAIN |
452+
Consts.LIBSSH2_KNOWNHOST_KEYENC_RAW
474453
session = @ccall "libssh2".libssh2_session_init_ex(
475454
C_NULL :: Ptr{Cvoid},
476455
C_NULL :: Ptr{Cvoid},
@@ -492,46 +471,32 @@ function ssh_knownhost_check(
492471
@ccall "libssh2".libssh2_knownhost_free(hosts::Ptr{Cvoid})::Cvoid
493472
continue
494473
end
495-
name_match = false
496-
prev = Ptr{KnownHost}(0)
497-
store = Ref{Ptr{KnownHost}}()
498-
while true
499-
get = @ccall "libssh2".libssh2_knownhost_get(
500-
hosts :: Ptr{Cvoid},
501-
store :: Ptr{Ptr{KnownHost}},
502-
prev :: Ptr{KnownHost},
503-
) :: Cint
504-
get < 0 && @warn("Error searching SSH known hosts file `$file`")
505-
get == 0 || break # end of file or error
506-
# got a known hosts record for host, now check its key hash
507-
prev = store[]
508-
known_host = unsafe_load(prev)
509-
known_host.name == C_NULL && continue
510-
host == unsafe_string(known_host.name) || continue
511-
name_match = true # we've found some entry in this file
512-
key_match = true # all available hashes must match
513-
key = base64decode(unsafe_string(known_host.key))
514-
if hashes.sha1 !== nothing
515-
key_match &= sha1(key) == collect(hashes.sha1)
516-
end
517-
if hashes.sha256 !== nothing
518-
key_match &= sha256(key) == collect(hashes.sha256)
519-
end
520-
key_match || continue
521-
# name and key match found
474+
size = ncodeunits(key)
475+
check = @ccall "libssh2".libssh2_knownhost_checkp(
476+
hosts :: Ptr{Cvoid},
477+
host :: Cstring,
478+
port :: Cint,
479+
key :: Ptr{UInt8},
480+
size :: Csize_t,
481+
mask :: Cint,
482+
C_NULL :: Ptr{Ptr{KnownHost}},
483+
) :: Cint
484+
if check == Consts.LIBSSH2_KNOWNHOST_CHECK_MATCH ||
485+
check == Consts.LIBSSH2_KNOWNHOST_CHECK_MISMATCH
522486
@ccall "libssh2".libssh2_knownhost_free(hosts::Ptr{Cvoid})::Cvoid
523487
@assert 0 == @ccall "libssh2".libssh2_session_free(session::Ptr{Cvoid})::Cint
524-
return Consts.SSH_HOST_KNOWN
488+
return check
489+
else
490+
@ccall "libssh2".libssh2_knownhost_free(hosts::Ptr{Cvoid})::Cvoid
491+
if check == Consts.LIBSSH2_KNOWNHOST_CHECK_FAILURE
492+
@warn("Error searching SSH known hosts file `$file`")
493+
end
494+
continue
525495
end
526-
@ccall "libssh2".libssh2_knownhost_free(hosts::Ptr{Cvoid})::Cvoid
527-
name_match || continue # no name match, search more files
528-
# name match but no key match => host mismatch
529-
@assert 0 == @ccall "libssh2".libssh2_session_free(session::Ptr{Cvoid})::Cint
530-
return Consts.SSH_HOST_MISMATCH
531496
end
532497
# name not found in any known hosts files
533498
@assert 0 == @ccall "libssh2".libssh2_session_free(session::Ptr{Cvoid})::Cint
534-
return Consts.SSH_HOST_UNKNOWN
499+
return Consts.LIBSSH2_KNOWNHOST_CHECK_NOTFOUND
535500
end
536501

537502
"C function pointer for `mirror_callback`"

stdlib/LibGit2/src/consts.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,11 +330,11 @@ const LIBSSH2_KNOWNHOST_TYPE_CUSTOM = 3
330330
const LIBSSH2_KNOWNHOST_KEYENC_RAW = 1 << 16
331331
const LIBSSH2_KNOWNHOST_KEYENC_BASE64 = 2 << 16
332332

333-
# internal constants for SSH host verification outcomes
334-
const SSH_HOST_KNOWN = 0
335-
const SSH_HOST_UNKNOWN = 1
336-
const SSH_HOST_MISMATCH = 2
337-
const SSH_HOST_BAD_HASH = 3
333+
# libssh2 host check return values
334+
const LIBSSH2_KNOWNHOST_CHECK_MATCH = 0
335+
const LIBSSH2_KNOWNHOST_CHECK_MISMATCH = 1
336+
const LIBSSH2_KNOWNHOST_CHECK_NOTFOUND = 2
337+
const LIBSSH2_KNOWNHOST_CHECK_FAILURE = 3
338338

339339
@enum(GIT_SUBMODULE_IGNORE, SUBMODULE_IGNORE_UNSPECIFIED = -1, # use the submodule's configuration
340340
SUBMODULE_IGNORE_NONE = 1, # any change or untracked == dirty

0 commit comments

Comments
 (0)