diff --git a/src/ssl.jl b/src/ssl.jl index a469f2d..2d11b8c 100644 --- a/src/ssl.jl +++ b/src/ssl.jl @@ -126,6 +126,11 @@ function TLSServerMethod() end const SSL_MODE_AUTO_RETRY = 0x00000004 +const SSL_VERIFY_NONE = Cint(0x00) +const SSL_VERIFY_PEER = Cint(0x01) +const SSL_VERIFY_FAIL_IF_NO_PEER_CERT = Cint(0x02) +const SSL_VERIFY_CLIENT_ONCE = Cint(0x04) +const SSL_VERIFY_POST_HANDSHAKE = Cint(0x08) # Use NetworkOptions for default CA file so that it can be configured using the standard # environment variables (JULIA_SSL_CA_ROOTS_PATH, SSL_CERT_DIR, and SSL_CERT_FILE). @@ -174,27 +179,82 @@ end function ca_chain!(ssl_context::SSLContext, cacert::String) if isfile(cacert) - ccall( - (:SSL_CTX_load_verify_locations, libssl), - Cint, - (SSLContext, Ptr{Cchar}, Ptr{Cchar}), - ssl_context, - cacert, - C_NULL) + ssl_load_verify_locations(ssl_context, cacert, nothing) elseif isdir(cacert) - ccall( - (:SSL_CTX_load_verify_locations, libssl), - Cint, - (SSLContext, Ptr{Cchar}, Ptr{Cchar}), - ssl_context, - C_NULL, - cacert) + ssl_load_verify_locations(ssl_context, nothing, cacert) else ArgumentError("Invalid CA certificates location: $cacert") end end +""" + ssl_set_verify(ssl_context::SSLContext, mode::Integer) + +Sets certificate verification mode for new SSL connections created from this context. +""" +function ssl_set_verify(ssl_context::SSLContext, mode::Integer) + ccall( + (:SSL_CTX_set_verify, libssl), + Cvoid, + (SSLContext, Cint, Ptr{Cvoid}), + ssl_context, + Cint(mode), + C_NULL) + return nothing +end + +""" + ssl_load_verify_locations(ssl_context::SSLContext, cafile::Union{Nothing, String}, capath::Union{Nothing, String}) + +Loads trusted CA certificates from a file and/or directory. +""" +function ssl_load_verify_locations(ssl_context::SSLContext, cafile::Union{Nothing,String}, capath::Union{Nothing,String}) + return ccall( + (:SSL_CTX_load_verify_locations, libssl), + Cint, + (SSLContext, Cstring, Cstring), + ssl_context, + isnothing(cafile) ? C_NULL : cafile, + isnothing(capath) ? C_NULL : capath) +end + +""" + ssl_load_client_ca_file(cafile::String) + +Loads client CA names from a PEM file. Returns an opaque pointer to be passed +to `ssl_set_client_ca_list`. Used for mTLS server configuration to advertise +acceptable client CAs during the TLS handshake. +""" +function ssl_load_client_ca_file(cafile::String) + ca_list = ccall( + (:SSL_load_client_CA_file, libssl), + Ptr{Cvoid}, + (Cstring,), + cafile) + if ca_list == C_NULL + throw(OpenSSLError()) + end + return ca_list +end + +""" + ssl_set_client_ca_list(ssl_context::SSLContext, ca_list::Ptr{Cvoid}) + +Sets the list of CAs sent to the client when requesting a client certificate. +The `ca_list` should be obtained from `ssl_load_client_ca_file`. +The SSLContext takes ownership of `ca_list`; do not free it manually. +""" +function ssl_set_client_ca_list(ssl_context::SSLContext, ca_list::Ptr{Cvoid}) + ccall( + (:SSL_CTX_set_client_CA_list, libssl), + Cvoid, + (SSLContext, Ptr{Cvoid}), + ssl_context, + ca_list) + return nothing +end + function free(ssl_context::SSLContext) ssl_context.ssl_ctx == C_NULL && return ccall( @@ -388,6 +448,19 @@ function get_error(ssl::SSL, ret::Cint)::SSLErrorCode ret) end +""" + ssl_get_verify_result(ssl::SSL) + +Returns the result of certificate verification for this SSL connection. +""" +function ssl_get_verify_result(ssl::SSL) + return ccall( + (:SSL_get_verify_result, libssl), + Cint, + (SSL,), + ssl) +end + """ SSLStream. """ @@ -524,11 +597,7 @@ function Sockets.connect(ssl::SSLStream; require_ssl_verification::Bool=true) if require_ssl_verification Base.@lock ssl.lock begin ssl.closed && throwio(:verify_result) - if (ret = ccall( - (:SSL_get_verify_result, libssl), - Cint, - (SSL,), - ssl.ssl)) != 0 + if (ret = ssl_get_verify_result(ssl.ssl)) != 0 throw(OpenSSLError(unsafe_string(ccall( (:X509_verify_cert_error_string, libcrypto), Ptr{UInt8}, diff --git a/test/runtests.jl b/test/runtests.jl index 15479d2..30317fb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -195,6 +195,7 @@ end ssl = SSLStream(ssl_ctx, tcp_stream) OpenSSL.connect(ssl) + @test OpenSSL.ssl_get_verify_result(ssl.ssl) == 0 x509_server_cert = OpenSSL.get_peer_certificate(ssl) @@ -621,8 +622,8 @@ end @testset "CACertsLoading" begin certs_dir = joinpath(@__DIR__, "certs") - certs_path = joinpath(certs_dir, "ca-certificates.crt") - + certs_path = joinpath(certs_dir, "ca-certificates.crt") + ssl_method = OpenSSL.TLSClientMethod() ctx = OpenSSL.SSLContext(ssl_method, certs_path) @test typeof(ctx) == OpenSSL.SSLContext @@ -632,3 +633,31 @@ end @test_throws ErrorException OpenSSL.SSLContext(ssl_method, "does_not_exist") end + +@testset "SSLVerifyBindings" begin + certs_dir = joinpath(@__DIR__, "certs") + certs_path = joinpath(certs_dir, "ca-certificates.crt") + + ssl_method = OpenSSL.TLSServerMethod() + ctx = OpenSSL.SSLContext(ssl_method) + + mode = OpenSSL.SSL_VERIFY_PEER | OpenSSL.SSL_VERIFY_FAIL_IF_NO_PEER_CERT + OpenSSL.ssl_set_verify(ctx, mode) + + current_mode = ccall( + (:SSL_CTX_get_verify_mode, libssl), + Cint, + (OpenSSL.SSLContext,), + ctx) + @test current_mode == mode + + @test OpenSSL.ssl_load_verify_locations(ctx, certs_path, nothing) == 1 + @test OpenSSL.ssl_load_verify_locations(ctx, nothing, certs_dir) == 1 + + # Test loading client CA list for mTLS. + ca_list = OpenSSL.ssl_load_client_ca_file(certs_path) + @test ca_list != C_NULL + OpenSSL.ssl_set_client_ca_list(ctx, ca_list) + + OpenSSL.free(ctx) +end