Skip to content

Commit f48bb3d

Browse files
LibGit2: improve error when CA root cert can't be set
This also fixes an insecure behavior: even if `set_ssl_cert_locations` failed, `REFCOUNT` was still incremented, so subsequent calls to `ensure_initialized` didn't call `initialize` and so there is never a successful call to `set_ssl_cert_locations`. Without this libgit2 defaults to not verifying host identities, which is insecure. To prevent this, this patch locks on `ensure_initialized` and decrements `REFCOUNT` if initialize throws an error, ensuring that `initialize` succeeds at least once, including the call to `set_ssl_cert_locations`.
1 parent 99402b4 commit f48bb3d

File tree

5 files changed

+127
-16
lines changed

5 files changed

+127
-16
lines changed

stdlib/LibGit2/src/LibGit2.jl

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -961,13 +961,19 @@ end
961961

962962
## lazy libgit2 initialization
963963

964+
const ENSURE_INITIALIZED_LOCK = ReentrantLock()
965+
964966
function ensure_initialized()
965-
x = Threads.atomic_cas!(REFCOUNT, 0, 1)
966-
if x < 0
967-
negative_refcount_error(x)::Union{}
968-
end
969-
if x == 0
970-
initialize()
967+
lock(ENSURE_INITIALIZED_LOCK) do
968+
x = Threads.atomic_cas!(REFCOUNT, 0, 1)
969+
x > 0 && return
970+
x < 0 && negative_refcount_error(x)::Union{}
971+
try initialize()
972+
catch
973+
Threads.atomic_sub!(REFCOUNT, 1)
974+
@assert REFCOUNT[] == 0
975+
rethrow()
976+
end
971977
end
972978
return nothing
973979
end
@@ -979,24 +985,40 @@ end
979985
@noinline function initialize()
980986
@check ccall((:git_libgit2_init, :libgit2), Cint, ())
981987

988+
cert_loc = NetworkOptions.ca_roots()
989+
cert_loc !== nothing && set_ssl_cert_locations(cert_loc)
990+
982991
atexit() do
983992
# refcount zero, no objects to be finalized
984993
if Threads.atomic_sub!(REFCOUNT, 1) == 1
985994
ccall((:git_libgit2_shutdown, :libgit2), Cint, ())
986995
end
987996
end
988-
989-
cert_loc = NetworkOptions.ca_roots()
990-
cert_loc !== nothing && set_ssl_cert_locations(cert_loc)
991997
end
992998

993999
function set_ssl_cert_locations(cert_loc)
994-
cert_file = isfile(cert_loc) ? cert_loc : Cstring(C_NULL)
995-
cert_dir = isdir(cert_loc) ? cert_loc : Cstring(C_NULL)
996-
cert_file == C_NULL && cert_dir == C_NULL && return
997-
@check ccall((:git_libgit2_opts, :libgit2), Cint,
998-
(Cint, Cstring...),
999-
Cint(Consts.SET_SSL_CERT_LOCATIONS), cert_file, cert_dir)
1000+
cert_file = cert_dir = Cstring(C_NULL)
1001+
if isdir(cert_loc) # directories
1002+
cert_dir = cert_loc
1003+
else # files, /dev/null, non-existent paths, etc.
1004+
cert_file = cert_loc
1005+
end
1006+
ret = ccall((:git_libgit2_opts, :libgit2), Cint, (Cint, Cstring...),
1007+
Cint(Consts.SET_SSL_CERT_LOCATIONS), cert_file, cert_dir)
1008+
ret >= 0 && return ret
1009+
err = Error.GitError(ret)
1010+
err.class == Error.SSL &&
1011+
err.msg == "TLS backend doesn't support certificate locations" ||
1012+
throw(err)
1013+
var = nothing
1014+
for v in NetworkOptions.CA_ROOTS_VARS
1015+
haskey(ENV, v) && (var = v)
1016+
end
1017+
@assert var !== nothing # otherwise we shouldn't be here
1018+
msg = """
1019+
Your Julia is built with a SSL/TLS engine that libgit2 doesn't know how to configure to use a file or directory of certificate authority roots, but your environment specifies one via the $var variable. If you believe your system's root certificates are safe to use, you can `export JULIA_SSL_CA_ROOTS_PATH=""` in your environment to use those instead.
1020+
"""
1021+
throw(Error.GitError(err.class, err.code, chomp(msg)))
10001022
end
10011023

10021024
end # module
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# This file is a part of Julia. License is MIT: https://julialang.org/license
2+
3+
module Test_LibGit2_https
4+
5+
using LibGit2, Test
6+
7+
ENV["JULIA_SSL_CA_ROOTS_PATH"] = joinpath(@__DIR__, "bad_ca_roots.pem")
8+
9+
mktempdir() do dir
10+
repo_url = "https://github.com/JuliaLang/Example.jl"
11+
12+
@testset "HTTPS clone with bad CA roots fails" begin
13+
repo_path = joinpath(dir, "Example.HTTPS")
14+
c = LibGit2.CredentialPayload(allow_prompt=false, allow_git_helpers=false)
15+
redirect_stderr(devnull)
16+
err = try LibGit2.clone(repo_url, repo_path, credentials=c)
17+
catch err
18+
err
19+
end
20+
@test err isa LibGit2.GitError
21+
@test err.msg == "user rejected certificate for github.com"
22+
end
23+
end
24+
25+
end # module
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDtDCCApwCCQDeWk9ywtjrpTANBgkqhkiG9w0BAQsFADCBmzELMAkGA1UEBhMC
3+
VVMxETAPBgNVBAgMCE5ldyBZb3JrMREwDwYDVQQHDAhOZXcgWW9yazEnMCUGA1UE
4+
CgweVGhlIEp1bGlhIFByb2dyYW1taW5nIExhbmd1YWdlMRYwFAYDVQQDDA1qdWxp
5+
YWxhbmcub3JnMSUwIwYJKoZIhvcNAQkBFhZzZWN1cml0eUBqdWxpYWxhbmcub3Jn
6+
MB4XDTIwMTIxMTE3NTgxN1oXDTI1MTIxMDE3NTgxN1owgZsxCzAJBgNVBAYTAlVT
7+
MREwDwYDVQQIDAhOZXcgWW9yazERMA8GA1UEBwwITmV3IFlvcmsxJzAlBgNVBAoM
8+
HlRoZSBKdWxpYSBQcm9ncmFtbWluZyBMYW5ndWFnZTEWMBQGA1UEAwwNanVsaWFs
9+
YW5nLm9yZzElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAanVsaWFsYW5nLm9yZzCC
10+
ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANCFgRMFlNGIgmZtMzR+Xx+t
11+
cPXpYnw9sZXlGy4y+P+UVW5rnFtf+OL4WkcJykmL3n/iLBKpdrndhzL7zuc6lGVv
12+
G6u+Gvwg5uCZ4RqiFSPP9xK7tl7H+CwrtWL/2vF1wlYC228A+NMpPyQw4XtX1L8G
13+
xAvJbFz8JrJ+WH1wCmVpkxA6pnpK+DZ/QKPVwa/qhB80ur3bYwlHXWwDBf8bq98f
14+
7wDBpJoEc3IG3GYopP6ie5KTENKxbDZjr306ZuxTLjXKrAE/OJkAiGKJ7gPlwT/E
15+
kFI/x/No9Y/fPWFRGiFo2L4fhP2Mohcph3PQswFKfnQlMQzztetDKWCZveB5HisC
16+
AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAqAaFA93Q3VWWKAZBqORT+6N2iHDiOxMu
17+
Ol8Jjqp3Spj552NbyPPpfF2a2Q/Bh2ZAmncCoGTpuXdnowSHyXuxPey6BIvEbq0L
18+
FizTNuIzaA95fO/ce9LNujxliDHhKMJBZtCqBJYJ4dgd9sA4/LeAG/P3ltIY6K8P
19+
22AAx2bzWbeRJSqxeBodm19rOb9Yz2SOaZIam42E+xia+hsUFdGf6Zkfpa02azDm
20+
93EjS+DwapqxAKgkps6JuKqpRFdZd8QsVmgAcapnIt77w8sfBu9eyITF/Tm+MA8k
21+
IRieSypM7TK0jQ6QrNV7FKSI6eEPaqWBMwkLg3S5H6KQMntVRlcc0A==
22+
-----END CERTIFICATE-----

stdlib/LibGit2/test/libgit2.jl

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@ module LibGit2Tests
44

55
import LibGit2
66
using Test
7-
using Random, Serialization, Sockets
7+
using Random, Serialization, Sockets, NetworkOptions
88

99
const BASE_TEST_PATH = joinpath(Sys.BINDIR, "..", "share", "julia", "test")
1010
isdefined(Main, :FakePTYs) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "FakePTYs.jl"))
1111
import .Main.FakePTYs: with_fake_pty
1212

13+
# we currently use system SSL/TLS on macOS and Windows platforms
14+
# and libgit2 cannot set the CA roots path on those systems
15+
# if that changes, this may need to be adjusted
16+
const CAN_SET_CA_ROOTS_PATH = !Sys.isapple() && !Sys.iswindows()
17+
1318
function challenge_prompt(code::Expr, challenges; timeout::Integer=60, debug::Bool=true)
1419
input_code = tempname()
1520
open(input_code, "w") do fp
@@ -168,6 +173,28 @@ end
168173
@test findfirst(isequal(LibGit2.Consts.FEATURE_HTTPS), f) !== nothing
169174
end
170175

176+
@testset "SSL/TLS initialization" begin
177+
withenv("JULIA_SSL_CA_ROOTS_PATH" => nothing) do
178+
# these fail for different reasons on different platforms:
179+
# - on Apple & Windows you cannot set the CA roots path location
180+
# - on Linux & FreeBSD you you can but these are invalid files
181+
ENV["JULIA_SSL_CA_ROOTS_PATH"] = "/dev/null"
182+
@test_throws LibGit2.GitError LibGit2.ensure_initialized()
183+
ENV["JULIA_SSL_CA_ROOTS_PATH"] = tempname()
184+
@test_throws LibGit2.GitError LibGit2.ensure_initialized()
185+
# test that it still fails if called a second time
186+
@test_throws LibGit2.GitError LibGit2.ensure_initialized()
187+
if !CAN_SET_CA_ROOTS_PATH
188+
# this would work on Linux & FreeBSD, but we don't want it
189+
# see `ca_roots.jl` for tests that don't affect behavior
190+
ENV["JULIA_SSL_CA_ROOTS_PATH"] = NetworkOptions.bundled_ca_roots()
191+
@test_throws LibGit2.GitError LibGit2.ensure_initialized()
192+
end
193+
end
194+
# should still be possible to initialize
195+
@test LibGit2.ensure_initialized() === nothing
196+
end
197+
171198
@testset "OID" begin
172199
z = LibGit2.GitHash()
173200
@test LibGit2.iszero(z)

stdlib/LibGit2/test/online.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ using Test
66
import LibGit2
77
using Random
88

9+
# we currently use system SSL/TLS on macOS and Windows platforms
10+
# and libgit2 cannot set the CA roots path on those systems
11+
# if that changes, this may need to be adjusted
12+
const CAN_SET_CA_ROOTS_PATH = !Sys.isapple() && !Sys.iswindows()
13+
914
function transfer_progress(progress::Ptr{LibGit2.TransferProgress}, payload::Dict)
1015
status = payload[:transfer_progress]
1116
progress = unsafe_load(progress)
@@ -90,4 +95,14 @@ mktempdir() do dir
9095
end
9196
end
9297

98+
if CAN_SET_CA_ROOTS_PATH
99+
# needs to be run in separate process so it can re-initialize libgit2
100+
# with a useless self-signed certificate authority root certificate
101+
file = joinpath(@__DIR__, "bad_ca_roots.jl")
102+
cmd = `$(Base.julia_cmd()) --depwarn=no --startup-file=no $file`
103+
if !success(pipeline(cmd; stdout=stdout, stderr=stderr))
104+
error("bad CA roots tests failed, cmd : $cmd")
105+
end
106+
end
107+
93108
end # module

0 commit comments

Comments
 (0)