diff --git a/src/hackney_conn.erl b/src/hackney_conn.erl index c2b3e796..6876b089 100644 --- a/src/hackney_conn.erl +++ b/src/hackney_conn.erl @@ -761,10 +761,9 @@ connected({call, From}, {upgrade_to_ssl, _SslOpts}, #conn_data{transport = hackn {keep_state_and_data, [{reply, From, ok}]}; connected({call, From}, {upgrade_to_ssl, SslOpts}, #conn_data{socket = Socket, host = Host, connect_options = ConnectOpts} = Data) -> %% Upgrade TCP socket to SSL (e.g., after CONNECT proxy tunnel) - %% Get default SSL options with hostname verification - DefaultSslOpts = hackney_ssl:check_hostname_opts(Host), - %% Merge user-provided SSL options (they override defaults) - MergedSslOpts = hackney_util:merge_opts(DefaultSslOpts, SslOpts), + %% Use ssl_opts/2 to properly merge defaults with user options + %% (handles cacertfile vs cacerts correctly) + MergedSslOpts = hackney_ssl:ssl_opts(Host, [{ssl_options, SslOpts}]), %% Add ALPN options for HTTP/2 negotiation %% Check both SslOpts (from upgrade call) and ConnectOpts (from initial config) AlpnOpts = case hackney_ssl:alpn_opts(SslOpts) of @@ -2271,8 +2270,9 @@ do_tcp_connect(From, Data) -> TransportOpts = proplists:delete(protocols, ConnectOpts), Opts = case Transport of hackney_ssl -> - DefaultSslOpts = hackney_ssl:check_hostname_opts(Host), - MergedSslOpts = hackney_util:merge_opts(DefaultSslOpts, SslOpts0), + %% Use ssl_opts/2 to properly merge defaults with user options + %% (handles cacertfile vs cacerts correctly) + MergedSslOpts = hackney_ssl:ssl_opts(Host, [{ssl_options, SslOpts0}]), AlpnOpts = hackney_ssl:alpn_opts(ConnectOpts), FinalSslOpts = hackney_util:merge_opts(MergedSslOpts, AlpnOpts), TransportOpts ++ [{ssl_options, FinalSslOpts}]; diff --git a/test/certs/ca.key b/test/certs/ca.key new file mode 100644 index 00000000..383274cc --- /dev/null +++ b/test/certs/ca.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCj1XbtZLafXR5i +h513pkjAHfj+Ox8d5BgayydNru4zZVMdsamxZO02ZAvhM2CBlJ0T2XLuxE/4ULUD +1nzH/58gz4ij78KMU5l77LRa2IwWYQSTotLIHR5CVkba8khgsj7sKrlOsvcEJW5+ +jl5Eov6a5LMr8eB13wBlWeqrID0kwo5Ebw+soTzUrJn57u5FsUzklm4N+W+WgwR+ +KawGss0Qg2YNA101tlIDWwH8VBqXxnr76T8fTdoEK+dqNYTUq2U7+HJlhHTJchj0 +OesoS4gZfat244MsyFagrZNPsTrofOeO5vRiKgCOss0iAnrn6/yjfiuoJ+1RqVYH +FF9f9DFxAgMBAAECggEABqrerctYG8z3OjkM7WRC6G+n1RfcnLOuT/PV2AVfHBAa +9XXII0gpV7nzWlFEE43z5Q2HzgQHAaLuNPdnCWAjrpsHk21j4GBcGhUgY2SV8ei1 +nhkFpT97HmXCuTEcRTQn16Zm93cU0rI/yI58c2RjQnQ9fvO3Z/ChBF7oDBoSJvNj +zZlhkrfhCJgujf5deQjk2td5zVDKBB7OkrBHM7nGdOyqPWwRBWHRrpCj2v2MKQOu +LjNcBwr7qE0jYA7D5y2M3AI2Bpof7IgFhnnvN9xkj8e6uvfMjd2y4gpOvxwdf3XO +ya+vAtcbI1PVgIj8KY6rHgMXtqkYfmfn9fq1Zy5pAQKBgQDc162OFK8twi9rmz9w ++H3dv8dEOtTbnviITEb9X8z7j6QcYpf3oJVcfGkNaz2Co4g/MDzkK7RD6XHeUvBo +T28JvskghVNMYO6HKf/2oiTPlwtH1Wdm6I0n5fo8L29a8IkXlnfpNmstI0vJsXXo +L5+pIyNLv6L9KfzhEq+UYK+ngQKBgQC96m24V7OAXw0YFbjEX/2P4CMlq2Wfe06r +OiqVmZH4pUASQ0q1N8CMrSCu1Y7MZzLaUuLdL8GwL0NGZMBMSimTt7pfZDDeDda4 +xX22fYas+ZUZJAHu7CdvHH1MT9GEDp+G7XVY/LnruMSNwJ/lFOobNrcqRWz2ojru +44IjovAB8QKBgGAYRT/Oxk8t8P5sxlU8+1/TRDzvMJIEAXclYbp8xjAsV6e2SxQI +PxXIWNnq8Q/4Yp/EOKq8TatDWDX6dvucnN9rsg7BlPZmM0SDRQqnkUb3HYR7WowP +4uQakSFBLr4ubijiY3kKIea5NhAkdP68QkgRrxkV4TEx5QR24gm5bJWBAoGAY9tO +g54BcN8JiH9rXj3Gmg7VDCp5zYhNTfTQjUZpHR7ueGvPbUd6Q72IMMVzRwCAGZF5 +XamNovDG48132uUnxVbWdO++ThNislaNChYoaOz2O3jWV2TuOxr0utpBJLl3ob9b +c0W3ED1fg9UjfZUontR/LIfCik+0wwT22XwDzFECgYBqcL9JWozhqVVOmEO6PqZe +ysVWZInS9REFyOTBz7C3csUnzX+7aFljjSB7H8BeJ/Q4rmvNJ33ueqnXAg/CnyDO +Ex+GiFHceCk5j5eS0r/uKhyrfL3wysYTDfOoN585n8unEJsif4hkRi6C52OhedwS +Sg+tHFiu58ngrgxCrniA2Q== +-----END PRIVATE KEY----- diff --git a/test/certs/ca.pem b/test/certs/ca.pem new file mode 100644 index 00000000..372ccd97 --- /dev/null +++ b/test/certs/ca.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDBTCCAe2gAwIBAgIUa2T0vunNiiG+w74wOqoY/wpfW1cwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAzMTgwNzI3NDFaFw0zNjAzMTUw +NzI3NDFaMBIxEDAOBgNVBAMMB1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCj1XbtZLafXR5ih513pkjAHfj+Ox8d5BgayydNru4zZVMdsamx +ZO02ZAvhM2CBlJ0T2XLuxE/4ULUD1nzH/58gz4ij78KMU5l77LRa2IwWYQSTotLI +HR5CVkba8khgsj7sKrlOsvcEJW5+jl5Eov6a5LMr8eB13wBlWeqrID0kwo5Ebw+s +oTzUrJn57u5FsUzklm4N+W+WgwR+KawGss0Qg2YNA101tlIDWwH8VBqXxnr76T8f +TdoEK+dqNYTUq2U7+HJlhHTJchj0OesoS4gZfat244MsyFagrZNPsTrofOeO5vRi +KgCOss0iAnrn6/yjfiuoJ+1RqVYHFF9f9DFxAgMBAAGjUzBRMB0GA1UdDgQWBBTc +WObZ/TNAq4tQqy34nNCttoc5FDAfBgNVHSMEGDAWgBTcWObZ/TNAq4tQqy34nNCt +toc5FDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAxnuB+uBwK +jzS/muqhi8l3ccGh8WvQy0ABLVBbxQ+IElTNCxA+zh6zCnMdmv7LqN7Q1sFcTsoq +aSsyvC5TPS6eVSuh4eB5ATY5bZD0J+X35OWd0+1vyDHFQN7fjgVGz09OFCbUyRfi +U934qbTEcD3gpv9LKs9WQnuKKouAPlIqrIDak/z9o4PsYP52Av2h3O+ThO+suBbD +a9IoNHru+I46xrqXsze8BzIVdnPYVpUxtsF440cErBeKonDzrQAWNIonut3ldsgG +JxvJQFl+itRAcKhOI9GvN2nSstkbYC6VaZ0EpzPqhfazuXz4Re4Te5NGa8/j78GI +V3UA40EJUz7c +-----END CERTIFICATE----- diff --git a/test/certs/server.key b/test/certs/server.key new file mode 100644 index 00000000..b8322741 --- /dev/null +++ b/test/certs/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCoQRXFhsXPwITJ +xvXpm6ShxKIE1BqSCqoaVPycpEHPEr0S3xTXgTfp53Xv0p2L65bucF2Yn4+1wonf +qAh796po/8jSI+Fzoh51tHs5OTvEGcL2qHUFBXvdpleQuf4MeMU3ouDx1zMrXjuw +DvCdwzCkOj6fA2v7+4BPfQATMosOy3vbTJKHv/HKXteg/Nbp1TZOwl3Pr9zGYXFw +5+6k2Qh+cdg/l3qT2HxhMBSKtlmmn5X4ofmjWOelly1v8WpCn5psjfvuoJnitA4U +Xc3dawdYp3/0rL+/BWTbXDDqxN7TE5DsTOhmDlWmCV8a1QSpzz8V+2MTEJ2F5+jr +oldmmMDrAgMBAAECggEARrQzBfKApbDtHC2zoRt6r1AGFalcEQrSOIaGMP0FepMR +SSDdjUIL0QsnEESdV/MEVeZ6Lmy+406Aya+/APkubzktlsDlOMZjrmrNbVqTtvBs +cWKQ6i9HwfjoyzSdgXguyBZ2GKqqIgtTYcSlcGZZxmmDbybs9dLWNJD+uxJ+RRSm +Qt/UYrPk9Ldkzze9wDRTv5bnMDgOayah4nYITBIsmbFEd+QBQYkjjzDecwHA3JrC +7qkRHK/YXerAUDfSu+kiilwaG9rL01p+GGduoB3T0/Ba/+xryOTiJxHAv/lbiG1C +cjOQBdj2NZvt37dZ56On7fZelh/z0VAxsNL5VbH24QKBgQDiDYTi+llYDQclnjf1 +7owq+GNLtnqkOoxgCLL09H/4yvhpGRyl/sTWcJUmN30BIGf3JbsEdUlw8LpjbQqg +xG/+quoJzWhUdERE7ws077q7FS6oIuWR+Ebne010jOxR53K9TtG5fGka6krxmt/m +k5ZyGwoKWWrZaUVhUi1uEZTVvwKBgQC+i1pgcS1Exb4Ux6TkoD5pHKPLf1rJo4t5 +gd0NAQwzBoWvKtzoFrWHmJc8EwumZjTylRFsCQ4dhunRwTyAga6st1qBp1102yLX +lTCfLeXz0+LlkwUNhu7fbOLmSsn6xwiTXoNONQE3xabSYO+i/FQYtJwVH4phwMHi +NARqPKFX1QKBgCwL+lLH+VTA5R2dYMYY/1L4J1D/c5JAnk2wJD66zZzK3/CKphxq +Miyer1FNCpyHlfqAbZqGyBKrtYXeH24IGNKEtynFzoh2Rz8vXP2poLcHf5nfguAY +gqhkTEljlEC5WpAspY0BAvHtqUC+rtYc9/mv7xrpJXrLmmtGOffykQ+9AoGAb5rL +usVPkIKKDT3KdSbupz5hKeZUVNp37RmFUgKVFKXzU2A1t7LlbKCRpFw7bKFczeFG +LRM4s068UWFvgI10tDFIz7wp3zIjPEZkDjgiAijPM0xjn0KzUyZB2EVh/ILroPWw +zvP43KPmTD7+3WYSE85lxXGN6ieu6EEzfM46amkCgYAyRF2RA/CsbdK0+aAQpcVB +51H36BpItwcGh2/CF84hEm7Xq8FZN2Xv0a3aZfMD/4gWIDR5GN/HqBhIe0rzSV9B +ijVoF8P8JuYkB9FbZnK5srLqrh/uZb3AsHOuEzt3YnF1Xkq2h+UggE3e6izA5tXh +32zpCo8kSaPbLDyQR191og== +-----END PRIVATE KEY----- diff --git a/test/certs/server.pem b/test/certs/server.pem new file mode 100644 index 00000000..11233cdb --- /dev/null +++ b/test/certs/server.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDEjCCAfqgAwIBAgIUeHOpIj5w7HfcnQw9T0ShnnZ3BjMwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAeFw0yNjAzMTgwNzI3NDFaFw0zNjAzMTUw +NzI3NDFaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKhBFcWGxc/AhMnG9embpKHEogTUGpIKqhpU/JykQc8SvRLf +FNeBN+nnde/SnYvrlu5wXZifj7XCid+oCHv3qmj/yNIj4XOiHnW0ezk5O8QZwvao +dQUFe92mV5C5/gx4xTei4PHXMyteO7AO8J3DMKQ6Pp8Da/v7gE99ABMyiw7Le9tM +koe/8cpe16D81unVNk7CXc+v3MZhcXDn7qTZCH5x2D+XepPYfGEwFIq2Waaflfih ++aNY56WXLW/xakKfmmyN++6gmeK0DhRdzd1rB1inf/Ssv78FZNtcMOrE3tMTkOxM +6GYOVaYJXxrVBKnPPxX7YxMQnYXn6OuiV2aYwOsCAwEAAaNeMFwwGgYDVR0RBBMw +EYcEfwAAAYIJbG9jYWxob3N0MB0GA1UdDgQWBBQGilSdZDqBe3dZwUVJU+dZSwWF +ZTAfBgNVHSMEGDAWgBTcWObZ/TNAq4tQqy34nNCttoc5FDANBgkqhkiG9w0BAQsF +AAOCAQEALQzkn2Ggt2UhtqpLDMaYwY3h5+beqOVeZ8MYp+IOeJWNfDF072TE6pYH +xiZYfJ+RdZePe7VqnQJEQsMMJ/6p5VEGSCNN0Zryz4by3Vu/JA3RX6JqRq55Al9I +1CPwn2Ii+dE8CJYRXG63tqqYzchmPVwfing7/YpT8D7zUpRmsd9oU8PTlpnxonvc +SmvZEBTf8PVEm6HlO/nPxOi0MvN8AkQ8EBH23qDWPsiAd6LN+nySVnukAPRlt+8Q +ZTM6KneidFluqTzbacnw/9njlH7fyQktsUDQh3wzQ6+x10PjfFa25Vi1L+kRvk7x +e3h+4kGHJQpRtEBQ3NoeaS0KccGahg== +-----END CERTIFICATE----- diff --git a/test/hackney_cacertfile_bug_test.erl b/test/hackney_cacertfile_bug_test.erl new file mode 100644 index 00000000..dceec205 --- /dev/null +++ b/test/hackney_cacertfile_bug_test.erl @@ -0,0 +1,59 @@ +%%% Test that cacertfile option is respected when making HTTPS requests. +%%% +%%% Currently fails because hackney_conn.erl bypasses hackney_ssl:ssl_opts/2 +%%% and calls hackney_ssl:check_hostname_opts/1 directly, which always sets +%%% {cacerts, certifi:cacerts()}. Erlang SSL then ignores cacertfile. + +-module(hackney_cacertfile_bug_test). +-include_lib("eunit/include/eunit.hrl"). + +-define(HTTPS_PORT, 8126). + +cacertfile_test_() -> + {setup, + fun setup/0, + fun teardown/1, + [ + {"cacertfile respected in ssl_opts/2", fun test_ssl_opts_handles_cacertfile/0}, + {"cacertfile respected in request", fun test_request_with_cacertfile/0} + ]}. + +setup() -> + error_logger:tty(false), + {ok, _} = application:ensure_all_started(cowboy), + {ok, _} = application:ensure_all_started(hackney), + + CertDir = cert_dir(), + CertFile = filename:join(CertDir, "server.pem"), + KeyFile = filename:join(CertDir, "server.key"), + + Dispatch = cowboy_router:compile([{'_', [{"/[...]", test_http_resource, []}]}]), + {ok, _} = cowboy:start_tls(cacertfile_test_server, + [{certfile, CertFile}, + {keyfile, KeyFile}, + {port, ?HTTPS_PORT}], + #{env => #{dispatch => Dispatch}}), + ok. + +teardown(_) -> + cowboy:stop_listener(cacertfile_test_server), + application:stop(cowboy), + application:stop(hackney), + error_logger:tty(true), + ok. + +test_ssl_opts_handles_cacertfile() -> + CACertFile = filename:join(cert_dir(), "ca.pem"), + Options = [{ssl_options, [{cacertfile, CACertFile}]}], + SslOpts = hackney_ssl:ssl_opts("localhost", Options), + ?assert(lists:keymember(cacertfile, 1, SslOpts)), + ?assertNot(lists:keymember(cacerts, 1, SslOpts)). + +test_request_with_cacertfile() -> + CACertFile = filename:join(cert_dir(), "ca.pem"), + Url = "https://localhost:" ++ integer_to_list(?HTTPS_PORT) ++ "/get", + Opts = [{ssl_options, [{cacertfile, CACertFile}]}, {pool, false}], + {ok, 200, _, _} = hackney:request(get, Url, [], <<>>, Opts). + +cert_dir() -> + filename:join([filename:dirname(code:which(?MODULE)), "..", "test", "certs"]).