From c051cfa93906def8e47c6f110244e56394f88263 Mon Sep 17 00:00:00 2001 From: Benoit Chesneau Date: Mon, 19 Jan 2026 17:55:29 +0100 Subject: [PATCH] feat: add peercert/1 function to get peer SSL certificate (#599) Add hackney:peercert/1 function that returns the DER-encoded peer certificate for SSL connections, similar to how peername/1 returns the peer address. The function returns: - {ok, Cert} where Cert is the DER-encoded certificate binary - {error, not_ssl} for non-SSL connections - {error, not_connected} if there's no active connection Fixes #599 --- src/hackney.erl | 8 ++++++++ src/hackney_conn.erl | 19 +++++++++++++++++++ src/hackney_ssl.erl | 8 ++++++++ 3 files changed, 35 insertions(+) diff --git a/src/hackney.erl b/src/hackney.erl index b3e30403..12259e22 100644 --- a/src/hackney.erl +++ b/src/hackney.erl @@ -11,6 +11,7 @@ -export([connect/1, connect/2, connect/3, connect/4, close/1, peername/1, + peercert/1, sockname/1, request/1, request/2, request/3, request/4, request/5, send_request/2, @@ -401,6 +402,13 @@ normalize_transport(Other) -> Other. peername(ConnPid) when is_pid(ConnPid) -> hackney_conn:peername(ConnPid). +%% @doc Get the peer SSL certificate. +%% Returns the DER-encoded certificate of the peer, or an error if the connection +%% is not SSL or the certificate is unavailable. +-spec peercert(conn()) -> {ok, binary()} | {error, term()}. +peercert(ConnPid) when is_pid(ConnPid) -> + hackney_conn:peercert(ConnPid). + %% @doc Get the local address and port. -spec sockname(conn()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}. sockname(ConnPid) when is_pid(ConnPid) -> diff --git a/src/hackney_conn.erl b/src/hackney_conn.erl index 0a0f61a3..4c65e662 100644 --- a/src/hackney_conn.erl +++ b/src/hackney_conn.erl @@ -53,6 +53,7 @@ %% Socket operations setopts/2, peername/1, + peercert/1, sockname/1, %% Low-level socket operations (for hackney_request/hackney_response) send/2, @@ -352,6 +353,13 @@ setopts(Pid, Opts) -> peername(Pid) -> gen_statem:call(Pid, peername). +%% @doc Get the peer SSL certificate. +%% Returns {ok, Cert} where Cert is the DER-encoded certificate binary, +%% or {error, Reason} if the connection is not SSL or the certificate is unavailable. +-spec peercert(pid()) -> {ok, binary()} | {error, term()}. +peercert(Pid) -> + gen_statem:call(Pid, peercert). + %% @doc Get the local address and port. -spec sockname(pid()) -> {ok, {inet:ip_address(), inet:port_number()}} | {error, term()}. sockname(Pid) -> @@ -1405,6 +1413,14 @@ handle_common({call, From}, peername, _State, #conn_data{transport = Transport, Result = Transport:peername(Socket), {keep_state_and_data, [{reply, From, Result}]}; +handle_common({call, From}, peercert, _State, #conn_data{transport = Transport, socket = Socket} = _Data) + when Socket =/= undefined -> + Result = case erlang:function_exported(Transport, peercert, 1) of + true -> Transport:peercert(Socket); + false -> {error, not_ssl} + end, + {keep_state_and_data, [{reply, From, Result}]}; + handle_common({call, From}, sockname, _State, #conn_data{transport = Transport, socket = Socket} = _Data) when Socket =/= undefined -> Result = Transport:sockname(Socket), @@ -1443,6 +1459,9 @@ handle_common({call, From}, {setopts, _Opts}, _State, _Data) -> handle_common({call, From}, peername, _State, _Data) -> {keep_state_and_data, [{reply, From, {error, not_connected}}]}; +handle_common({call, From}, peercert, _State, _Data) -> + {keep_state_and_data, [{reply, From, {error, not_connected}}]}; + handle_common({call, From}, sockname, _State, _Data) -> {keep_state_and_data, [{reply, From, {error, not_connected}}]}; diff --git a/src/hackney_ssl.erl b/src/hackney_ssl.erl index dd2aa62d..c76d30fa 100644 --- a/src/hackney_ssl.erl +++ b/src/hackney_ssl.erl @@ -15,6 +15,7 @@ setopts/2, controlling_process/2, peername/1, + peercert/1, close/1, shutdown/2, sockname/1]). @@ -237,6 +238,13 @@ controlling_process(Socket, Pid) -> peername(Socket) -> ssl:peername(Socket). +%% @doc Return the peer certificate of an SSL connection. +%% @see ssl:peercert/1 +-spec peercert(ssl:sslsocket()) -> + {ok, binary()} | {error, atom()}. +peercert(Socket) -> + ssl:peercert(Socket). + %% @doc Close a TCP socket. %% @see ssl:close/1 -spec close(ssl:sslsocket()) -> ok.