@@ -359,19 +359,179 @@ function fetchhead_foreach_callback(ref_name::Cstring, remote_url::Cstring,
359
359
return Cint (0 )
360
360
end
361
361
362
+ 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
+ )
381
+ end
382
+
383
+ function verify_host_error (message:: AbstractString )
384
+ printstyled (stderr , " $message \n " , color = :cyan , bold = true )
385
+ end
386
+
362
387
function certificate_callback (
363
- cert_p :: Ptr{Cvoid } ,
388
+ cert_p :: Ptr{CertHostKey } ,
364
389
valid :: Cint ,
365
390
host_p :: Ptr{Cchar} ,
366
- user_p :: Ptr{Cvoid} ,
391
+ data_p :: Ptr{Cvoid} ,
367
392
):: Cint
368
393
valid != 0 && return Consts. CERT_ACCEPT
369
394
host = unsafe_string (host_p)
370
395
cert_type = unsafe_load (convert (Ptr{Cint}, cert_p))
371
396
transport = cert_type == Consts. CERT_TYPE_TLS ? " TLS" :
372
397
cert_type == Consts. CERT_TYPE_SSH ? " SSH" : nothing
373
- verify = NetworkOptions. verify_host (host, transport)
374
- verify ? Consts. PASSTHROUGH : Consts. CERT_ACCEPT
398
+ if ! NetworkOptions. verify_host (host, transport)
399
+ # user has opted out of host verification
400
+ return Consts. CERT_ACCEPT
401
+ end
402
+ if transport == " TLS"
403
+ # TLS verification is done before the callback and indicated with the
404
+ # incoming `valid` flag, so if we get here then host verification failed
405
+ verify_host_error (" TLS host verification: the identity of the server `$host ` could not be verified. Someone could be trying to man-in-the-middle your connection. It is also possible that the correct server is using an invalid certificate or that your system's certificate authority root store is misconfigured." )
406
+ return Consts. CERT_REJECT
407
+ elseif transport == " SSH"
408
+ # SSH verification has to be done here
409
+ files = [joinpath (homedir (), " .ssh" , " known_hosts" )]
410
+ check = ssh_knownhost_check (files, host, KeyHashes (cert_p))
411
+ valid = false
412
+ if check == Consts. SSH_HOST_KNOWN
413
+ valid = true
414
+ elseif check == Consts. SSH_HOST_UNKNOWN
415
+ if Sys. which (" ssh-keyscan" ) != = nothing
416
+ msg = " Please run `ssh-keyscan $host >> $(files[1 ]) ` in order to add the server to your known hosts file and the try again."
417
+ else
418
+ 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."
419
+ end
420
+ verify_host_error (" SSH host verification: the server `$host ` is not a known host. $msg " )
421
+ elseif check == Consts. SSH_HOST_MISMATCH
422
+ 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." )
425
+ else
426
+ @error (" unexpected SSH known host check result" , check)
427
+ end
428
+ return valid ? Consts. CERT_ACCEPT : Consts. CERT_REJECT
429
+ end
430
+ @error (" unexpected transport encountered, refusing to validate" , cert_type)
431
+ return Consts. CERT_REJECT
432
+ end
433
+
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
+
459
+ struct KnownHost
460
+ magic :: Cuint
461
+ node :: Ptr{Cvoid}
462
+ name :: Ptr{Cchar}
463
+ key :: Ptr{Cchar}
464
+ type :: Cint
465
+ end
466
+
467
+ function ssh_knownhost_check (
468
+ files :: AbstractVector{<:AbstractString} ,
469
+ host :: AbstractString ,
470
+ hashes :: KeyHashes ,
471
+ )
472
+ hashes. sha1 === hashes. sha256 === nothing &&
473
+ return Consts. SSH_HOST_BAD_HASH
474
+ session = @ccall " libssh2" . libssh2_session_init_ex (
475
+ C_NULL :: Ptr{Cvoid} ,
476
+ C_NULL :: Ptr{Cvoid} ,
477
+ C_NULL :: Ptr{Cvoid} ,
478
+ C_NULL :: Ptr{Cvoid} ,
479
+ ) :: Ptr{Cvoid}
480
+ for file in files
481
+ ispath (file) || continue
482
+ hosts = @ccall " libssh2" . libssh2_knownhost_init (
483
+ session :: Ptr{Cvoid} ,
484
+ ) :: Ptr{Cvoid}
485
+ count = @ccall " libssh2" . libssh2_knownhost_readfile (
486
+ hosts :: Ptr{Cvoid} ,
487
+ file :: Cstring ,
488
+ 1 :: Cint , # standard OpenSSH format
489
+ ) :: Cint
490
+ if count < 0
491
+ @warn (" Error parsing SSH known hosts file `$file `" )
492
+ @ccall " libssh2" . libssh2_knownhost_free (hosts:: Ptr{Cvoid} ):: Cvoid
493
+ continue
494
+ 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
522
+ @ccall " libssh2" . libssh2_knownhost_free (hosts:: Ptr{Cvoid} ):: Cvoid
523
+ @assert 0 == @ccall " libssh2" . libssh2_session_free (session:: Ptr{Cvoid} ):: Cint
524
+ return Consts. SSH_HOST_KNOWN
525
+ 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
531
+ end
532
+ # name not found in any known hosts files
533
+ @assert 0 == @ccall " libssh2" . libssh2_session_free (session:: Ptr{Cvoid} ):: Cint
534
+ return Consts. SSH_HOST_UNKNOWN
375
535
end
376
536
377
537
" C function pointer for `mirror_callback`"
@@ -381,4 +541,4 @@ credentials_cb() = @cfunction(credentials_callback, Cint, (Ptr{Ptr{Cvoid}}, Cstr
381
541
" C function pointer for `fetchhead_foreach_callback`"
382
542
fetchhead_foreach_cb () = @cfunction (fetchhead_foreach_callback, Cint, (Cstring, Cstring, Ptr{GitHash}, Cuint, Any))
383
543
" C function pointer for `certificate_callback`"
384
- certificate_cb () = @cfunction (certificate_callback, Cint, (Ptr{Cvoid }, Cint, Ptr{Cchar}, Ptr{Cvoid}))
544
+ certificate_cb () = @cfunction (certificate_callback, Cint, (Ptr{CertHostKey }, Cint, Ptr{Cchar}, Ptr{Cvoid}))
0 commit comments