Skip to content

Commit

Permalink
feat(api): quicer:peercert/1 (#195)
Browse files Browse the repository at this point in the history
  • Loading branch information
qzhuyan authored Jul 4, 2023
1 parent 236f427 commit 472a253
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 4 deletions.
59 changes: 58 additions & 1 deletion c_src/quicer_connection.c
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,56 @@ static QUIC_STATUS handle_connection_event_resumption_ticket_received(
static QUIC_STATUS handle_connection_event_peer_certificate_received(
QuicerConnCTX *c_ctx, QUIC_CONNECTION_EVENT *Event);

ERL_NIF_TERM
peercert1(ErlNifEnv *env, __unused_parm__ int argc, const ERL_NIF_TERM argv[])
{
ERL_NIF_TERM ctx = argv[0];
ERL_NIF_TERM DerCert;
void *q_ctx;
QuicerConnCTX *c_ctx;
int len = 0;
unsigned char *tmp;
if (enif_get_resource(env, ctx, ctx_stream_t, &q_ctx))
{
c_ctx = ((QuicerStreamCTX *)q_ctx)->c_ctx;
}
else if (enif_get_resource(env, ctx, ctx_connection_t, &q_ctx))
{
c_ctx = (QuicerConnCTX *)q_ctx;
}
else
{
return ERROR_TUPLE_2(ATOM_BADARG);
}

assert(c_ctx);

if (!c_ctx->peer_cert)
{
return ERROR_TUPLE_2(ATOM_NO_PEERCERT);
}

if ((len = i2d_X509(c_ctx->peer_cert, NULL)) < 0)
{
// unlikely to happen
return ERROR_TUPLE_2(ATOM_ERROR_INTERNAL_ERROR);
}

unsigned char *data
= enif_make_new_binary(env, len, &DerCert);

if (!data)
{
return ERROR_TUPLE_2(ATOM_ERROR_NOT_ENOUGH_MEMORY);
}

// note, using tmp is mandatory, see doc for i2d_X590
tmp = data;

i2d_X509(c_ctx->peer_cert, &tmp);
return SUCCESS(DerCert);
}

void
dump_sslkeylogfile(_In_z_ const char *FileName,
_In_ QUIC_TLS_SECRETS TlsSecrets)
Expand Down Expand Up @@ -1497,11 +1547,18 @@ static QUIC_STATUS
handle_connection_event_peer_certificate_received(QuicerConnCTX *c_ctx,
QUIC_CONNECTION_EVENT *Event)
{
// @TODO peer_certificate_received
// Only with QUIC_CREDENTIAL_FLAG_INDICATE_CERTIFICATE_RECEIVED set
assert(QUIC_CONNECTION_EVENT_PEER_CERTIFICATE_RECEIVED == Event->Type);
// Validate against CA certificates using OpenSSL API:s
X509 *cert = (X509 *)Event->PEER_CERTIFICATE_RECEIVED.Certificate;

// Preserve cert in ctx
if (c_ctx->peer_cert)
{
X509_free(c_ctx->peer_cert);
}
c_ctx->peer_cert = X509_dup(cert);

X509_STORE_CTX *x509_ctx
= (X509_STORE_CTX *)Event->PEER_CERTIFICATE_RECEIVED.Chain;

Expand Down
3 changes: 3 additions & 0 deletions c_src/quicer_connection.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ async_handshake_1(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]);

QUIC_STATUS continue_connection_handshake(QuicerConnCTX *c_ctx);

ERL_NIF_TERM
peercert1(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]);

#endif // __QUICER_CONNECTION_H_
6 changes: 6 additions & 0 deletions c_src/quicer_ctx.c
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ init_c_ctx()
c_ctx->ssl_keylogfile = NULL;
c_ctx->is_closed = TRUE; // init
c_ctx->config_resource = NULL;
c_ctx->peer_cert = NULL;
return c_ctx;
}

Expand All @@ -95,6 +96,11 @@ deinit_c_ctx(QuicerConnCTX *c_ctx)
enif_release_resource(c_ctx->config_resource);
}
AcceptorQueueDestroy(c_ctx->acceptor_queue);

if (c_ctx->peer_cert)
{
X509_free(c_ctx->peer_cert);
}
enif_mutex_destroy(c_ctx->lock);
}

Expand Down
2 changes: 1 addition & 1 deletion c_src/quicer_ctx.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ typedef struct QuicerConnCTX
BOOLEAN is_closed;
uint32_t event_mask;
char *ssl_keylogfile;
void *reserved1;
X509 *peer_cert;
void *reserved2;
void *reserved3;
} QuicerConnCTX;
Expand Down
1 change: 1 addition & 0 deletions c_src/quicer_eterms.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ extern ERL_NIF_TERM ATOM_DGRAM_SEND_ERROR;
extern ERL_NIF_TERM ATOM_SOCKNAME_ERROR;
extern ERL_NIF_TERM ATOM_OWNER_DEAD;
extern ERL_NIF_TERM ATOM_NOT_OWNER;
extern ERL_NIF_TERM ATOM_NO_PEERCERT;

// msquic_linux.h 'errors'
extern ERL_NIF_TERM ATOM_ERROR_NO_ERROR;
Expand Down
3 changes: 3 additions & 0 deletions c_src/quicer_nif.c
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ ERL_NIF_TERM ATOM_DGRAM_SEND_ERROR;
ERL_NIF_TERM ATOM_SOCKNAME_ERROR;
ERL_NIF_TERM ATOM_OWNER_DEAD;
ERL_NIF_TERM ATOM_NOT_OWNER;
ERL_NIF_TERM ATOM_NO_PEERCERT;

// Mirror 'errors' in msquic_linux.h
ERL_NIF_TERM ATOM_ERROR_NO_ERROR;
Expand Down Expand Up @@ -423,6 +424,7 @@ ERL_NIF_TERM ATOM_UNDEFINED;
ATOM(ATOM_DGRAM_SEND_ERROR, dgram_send_error); \
ATOM(ATOM_OWNER_DEAD, owner_dead); \
ATOM(ATOM_NOT_OWNER, not_owner); \
ATOM(ATOM_NO_PEERCERT, no_peercert); \
\
ATOM(ATOM_ERROR_NO_ERROR, no_error); \
ATOM(ATOM_ERROR_CONTINUE, continue); \
Expand Down Expand Up @@ -1434,6 +1436,7 @@ static ErlNifFunc nif_funcs[] = {
{ "getopt", 3, getopt3, 0},
{ "setopt", 4, setopt4, 0},
{ "controlling_process", 2, controlling_process, 0},
{ "peercert", 1, peercert1, 0},
/* for DEBUG */
{ "get_conn_rid", 1, get_conn_rid1, 1},
{ "get_stream_rid", 1, get_stream_rid1, 1}
Expand Down
8 changes: 8 additions & 0 deletions src/quicer.erl
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
, get_stream_id/1
, getstat/2
, peername/1
, peercert/1
, listeners/0
, listener/1
, controlling_process/2
Expand Down Expand Up @@ -824,6 +825,13 @@ getstat(Conn, Cnts) ->
peername(Handle) ->
quicer_nif:getopt(Handle, param_conn_remote_address, false).

%% @doc Peer Cert in DER-encoded binary
%% mimic {@link ssl:peername/1}
-spec peercert(connection_handle() | stream_handle()) ->
{ok, Cert:: public_key:der_encoded()} | {error, any()}.
peercert(Handle) ->
quicer_nif:peercert(Handle).

%% @doc Return true if stream open flags has unidirectional flag set
-spec is_unidirectional(stream_open_flags()) -> boolean().
is_unidirectional(Flags) ->
Expand Down
6 changes: 6 additions & 0 deletions src/quicer_nif.erl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
, getopt/3
, setopt/4
, controlling_process/2
, peercert/1
]).

-export([ get_conn_rid/1
Expand Down Expand Up @@ -232,6 +233,11 @@ get_stream_rid(_Handle) ->
controlling_process(_H, _P) ->
erlang:nif_error(nif_library_not_loaded).

-spec peercert(connection_handle() | stream_handle()) ->
{ok, Cert:: public_key:der_encoded()} | {error, any()}.
peercert(_Handle) ->
erlang:nif_error(nif_library_not_loaded).

%% Internals
-spec locate_lib(file:name(), file:name()) ->
{ok, file:filename()} | {error, not_found}.
Expand Down
124 changes: 124 additions & 0 deletions test/quicer_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

-module(quicer_SUITE).
-include_lib("kernel/include/file.hrl").
-include_lib("public_key/include/public_key.hrl").
-include("quicer.hrl").


%% API
-export([all/0,
suite/0,
Expand Down Expand Up @@ -181,6 +183,12 @@
, tc_direct_send_over_conn/1
, tc_direct_send_over_conn_block/1
, tc_direct_send_over_conn_fail/1

%% TLS certs
, tc_peercert_client/1
, tc_peercert_client_nocert/1
, tc_peercert_server/1
, tc_peercert_server_nocert/1
%% testcase to verify env works
%% , tc_network/1
]).
Expand Down Expand Up @@ -3099,6 +3107,108 @@ tc_getopt_tls_handshake_info(Config) ->
ct:fail("timeout")
end.

tc_peercert_client(Config) ->
Port = select_port(),
Owner = self(),
{SPid, Ref} = spawn_monitor(
fun() ->
simple_conn_server_client_cert(Owner, Config, Port)
end),
receive listener_ready -> ok end,
{ok, Conn} = quicer:connect("localhost", Port,
default_conn_opts_client_cert(Config, "ca"),
5000),
{ok, {_, _}} = quicer:sockname(Conn),
{ok, PeerCert} = quicer:peercert(Conn),
OTPCert = public_key:pkix_decode_cert(PeerCert, otp),
Subject = {rdnSequence,
[[{'AttributeTypeAndValue', ?'id-at-countryName',"SE"}],
[{'AttributeTypeAndValue',
?'id-at-organizationName',
{utf8String,<<"NOBODYAB">>}}],
[{'AttributeTypeAndValue',
?'id-at-commonName',
{utf8String,<<"server">>}}]]},
?assertMatch({_, Subject},
pubkey_cert:subject_id(OTPCert)),
ok = quicer:close_connection(Conn),
SPid ! done,
ensure_server_exit_normal(Ref),
ok.

tc_peercert_client_nocert(Config) ->
Port = select_port(),
Owner = self(),
{SPid, Ref} = spawn_monitor(
fun() ->
simple_conn_server(Owner, Config, Port)
end),
receive listener_ready -> ok end,
{ok, Conn} = quicer:connect("localhost", Port,
default_conn_opts(),
5000),
{ok, {_, _}} = quicer:sockname(Conn),
?assertEqual({error, no_peercert}, quicer:peercert(Conn)),
ok = quicer:close_connection(Conn),
SPid ! done,
ensure_server_exit_normal(Ref),
ok.

tc_peercert_server(Config) ->
Port = select_port(),
Owner = self(),
{SPid, Ref} = spawn_monitor(
fun() ->
simple_conn_server_client_cert(Owner, Config, Port)
end),
receive listener_ready -> ok end,
{ok, Conn} = quicer:connect("localhost", Port,
default_conn_opts_client_cert(Config, "ca"),
5000),
SPid ! peercert,
PeerCert = receive
{SPid, peercert, Cert} ->
Cert
end,
OTPCert = public_key:pkix_decode_cert(PeerCert, otp),
ct:pal("client cert is ~p", [OTPCert]),
Subject = {rdnSequence,
[[{'AttributeTypeAndValue', ?'id-at-countryName',"SE"}],
[{'AttributeTypeAndValue',
?'id-at-organizationName',
{utf8String,<<"NOBODYAB">>}}],
[{'AttributeTypeAndValue',
?'id-at-commonName',
{utf8String,<<"client">>}}]]},
?assertMatch({_, Subject},
pubkey_cert:subject_id(OTPCert)),
ok = quicer:close_connection(Conn),
SPid ! done,
ensure_server_exit_normal(Ref),
ok.

tc_peercert_server_nocert(Config) ->
Port = select_port(),
Owner = self(),
{SPid, Ref} = spawn_monitor(
fun() ->
simple_conn_server(Owner, Config, Port)
end),
receive listener_ready -> ok end,
{ok, Conn} = quicer:connect("localhost", Port,
default_conn_opts(),
5000),
{ok, {_, _}} = quicer:sockname(Conn),
SPid ! peercert,
receive
{SPid, peercert, CertResp} ->
?assertEqual({error, no_peercert}, CertResp)
end,
ok = quicer:close_connection(Conn),
SPid ! done,
ensure_server_exit_normal(Ref),
ok.

%%% ====================
%%% Internal helpers
%%% ====================
Expand Down Expand Up @@ -3292,10 +3402,17 @@ simple_conn_server(Owner, Config, Port) ->
Owner ! listener_ready,
{ok, Conn} = quicer:accept(L, [], 1000),
{ok, Conn} = quicer:handshake(Conn),
simple_conn_server_loop(L, Conn, Owner).

simple_conn_server_loop(L, Conn, Owner) ->
receive
done ->
quicer:close_listener(L),
ok;
peercert ->
CertResp = quicer:peercert(Conn),
Owner ! {self(), peercert, CertResp},
simple_conn_server_loop(L, Conn, Owner);
{quic, shutdown, Conn} ->
quicer:close_connection(Conn),
quicer:close_listener(L)
Expand All @@ -3319,10 +3436,17 @@ simple_conn_server_client_cert(Owner, Config, Port) ->
Owner ! listener_ready,
{ok, Conn} = quicer:accept(L, [], 1000),
{ok, Conn} = quicer:handshake(Conn),
simple_conn_server_client_cert_loop(L, Conn, Owner).

simple_conn_server_client_cert_loop(L, Conn, Owner) ->
receive
done ->
quicer:close_listener(L),
ok;
peercert ->
{ok, PeerCert} = quicer:peercert(Conn),
Owner ! {self(), peercert, PeerCert},
simple_conn_server_client_cert_loop(L, Conn, Owner);
{quic, shutdown, Conn, _ErrorCode} ->
quicer:close_connection(Conn),
quicer:close_listener(L)
Expand Down
4 changes: 2 additions & 2 deletions test/quicer_test_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ gen_ca(Path, Name) ->
io_lib:format("openssl req -new -x509 -nodes "
"-newkey ec:~s "
"-keyout ~s -out ~s -days 3650 "
"-subj \"/C=SE/O=Internet Widgits Pty Ltd CA\"",
"-subj \"/C=SE/O=NOBODYAB\"",
[ECKeyFile, ca_key_name(Path, Name),
ca_cert_name(Path, Name)])),
os:cmd(Cmd).
Expand Down Expand Up @@ -85,7 +85,7 @@ gen_host_cert(H, CaName, Path, Opts) ->
"-keyout ~s -out ~s "
"-addext \"subjectAltName=DNS:~s\" "
"-addext keyUsage=digitalSignature,keyAgreement "
"-subj \"/C=SE/O=Internet Widgits Pty Ltd/CN=~s\"",
"-subj \"/C=SE/O=NOBODYAB/CN=~s\"",
[PasswordArg, ECKeyFile, HKey, HCSR, CN, CN])),
create_file(HEXT,
"keyUsage=digitalSignature,keyAgreement\n"
Expand Down

0 comments on commit 472a253

Please sign in to comment.