Implement mTLS authentication between client and server

This commit is contained in:
Camden Dixie O'Brien 2025-02-23 01:32:59 +00:00
parent 83ab6f7a20
commit ebf9afb4e1
9 changed files with 166 additions and 43 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
test/*

4
client/config.vapi Normal file
View File

@ -0,0 +1,4 @@
[CCode (cheader_filename = "config.h")]
namespace Config {
public const string CERT_DIR;
}

View File

@ -1,29 +1,41 @@
using Gtk; using Gtk;
public class Connection { public class Connection {
private InetSocketAddress host;
public signal void response_received(uint8[] response); public signal void response_received(uint8[] response);
public Connection() { private TlsClientConnection tls_client;
public Connection() throws Error {
var loopback = new InetAddress.loopback(SocketFamily.IPV6); var loopback = new InetAddress.loopback(SocketFamily.IPV6);
host = new InetSocketAddress(loopback, 12888); var host = new InetSocketAddress(loopback, 12888);
var db_type = TlsBackend.get_default().get_file_database_type();
const string ca_path = Config.CERT_DIR + "/ca.pem";
var db = Object.new(db_type, "anchors", ca_path) as TlsDatabase;
var cert =
new TlsCertificate.from_file(Config.CERT_DIR + "/client.pem");
var plain_client = new SocketClient();
var plain_connection = plain_client.connect(host);
tls_client = TlsClientConnection.new(plain_connection, host);
tls_client.set_database(db);
tls_client.set_certificate(cert);
tls_client.handshake();
} }
public async void send(uint8[] message) throws Error { public async void send(uint8[] message) throws Error {
var client = new SocketClient(); yield tls_client.output_stream.write_async(message);
var conn = yield client.connect_async(host);
yield conn.output_stream.write_async(message);
var response = new uint8[1024]; var response = new uint8[1024];
var len = yield conn.input_stream.read_async(response); var len = yield tls_client.input_stream.read_async(response);
response_received(response[0:len]); response_received(response[0:len]);
} }
} }
public class MainWindow : Gtk.ApplicationWindow { public class MainWindow : Gtk.ApplicationWindow {
private Connection connection; private Connection connection;
private Gtk.Button send_button;
private Gtk.Label response_label; private Gtk.Label response_label;
private Gtk.Button connect_button;
public MainWindow(Gtk.Application app, Connection connection) { public MainWindow(Gtk.Application app, Connection connection) {
Object(application: app); Object(application: app);
@ -35,7 +47,7 @@ public class MainWindow : Gtk.ApplicationWindow {
this.connection = connection; this.connection = connection;
connection.response_received.connect((response) => { connection.response_received.connect((response) => {
response_label.label = "Response: " + (string)response; response_label.label = "Response: " + (string)response;
}); });
var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10); var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10);
box.margin_start = 10; box.margin_start = 10;
@ -43,9 +55,9 @@ public class MainWindow : Gtk.ApplicationWindow {
box.margin_top = 10; box.margin_top = 10;
box.margin_bottom = 10; box.margin_bottom = 10;
connect_button = new Gtk.Button.with_label("Foo"); send_button = new Gtk.Button.with_label("Send");
connect_button.clicked.connect(on_connect_clicked); send_button.clicked.connect(on_send_clicked);
box.append(connect_button); box.append(send_button);
response_label = new Gtk.Label(""); response_label = new Gtk.Label("");
response_label.wrap = true; response_label.wrap = true;
@ -56,9 +68,9 @@ public class MainWindow : Gtk.ApplicationWindow {
present(); present();
} }
private async void on_connect_clicked() { private async void on_send_clicked() {
try { try {
yield connection.send("Bar".data); yield connection.send("Foo".data);
} catch (Error e) { } catch (Error e) {
response_label.label = "Error: " + e.message; response_label.label = "Error: " + e.message;
} }
@ -71,8 +83,13 @@ public class StudySystemClient : Gtk.Application {
} }
protected override void activate() { protected override void activate() {
var connection = new Connection(); try {
new MainWindow(this, connection); var connection = new Connection();
new MainWindow(this, connection);
} catch (Error e) {
stderr.printf("Failed to initialize connection: %s\n",
e.message);
}
} }
public static int main(string[] args) { public static int main(string[] args) {

View File

@ -10,9 +10,21 @@ add_project_arguments('-w', language: 'c')
gtk_dep = dependency('gtk4') gtk_dep = dependency('gtk4')
conf = configuration_data()
conf.set_quoted(
'CONFIG_CERT_DIR',
join_paths(meson.project_source_root(), '..', 'test'))
configure_file(
output: 'config.h',
configuration: conf
)
exe = executable( exe = executable(
'study-system-client', 'study-system-client',
'main.vala', [
'main.vala',
'config.vapi'
],
dependencies: [gtk_dep], dependencies: [gtk_dep],
c_args: ['-w'] c_args: ['-w']
) )

76
scripts/make-test-certs.sh Executable file
View File

@ -0,0 +1,76 @@
#!/bin/sh
set -eu
mkdir -p test
# Create CA config
cat > test/ca.cnf << EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
[req_distinguished_name]
commonName = Study System CA
[v3_ca]
basicConstraints = critical,CA:TRUE
keyUsage = critical,keyCertSign,cRLSign
EOF
# Create CA key and certificate
openssl genrsa -out test/ca.key 4096
openssl req -new -x509 -key test/ca.key -outform PEM -out test/ca.pem \
-config test/ca.cnf
# Create server key and CSR
cat > test/server.cnf << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
commonName = localhost
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation,digitalSignature,keyEncipherment
subjectAltName = @alt_names
[alt_names]
IP.1 = ::1
DNS.1 = localhost
EOF
openssl genrsa -out test/server.key 4096
openssl req -new -key test/server.key -out test/server.csr \
-config test/server.cnf
# Sign server certificate
openssl x509 -req -in test/server.csr -CA test/ca.pem -CAkey test/ca.key \
-CAcreateserial -out test/server_cert.pem \
-extensions v3_req -extfile test/server.cnf
# Create client key and CSR
cat > test/client.cnf << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
[req_distinguished_name]
commonName = Study System Client
[v3_req]
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
EOF
openssl genrsa -out test/client.key 4096
openssl req -new -key test/client.key -out test/client.csr \
-config test/client.cnf
# Sign client certificate
openssl x509 -req -in test/client.csr -CA test/ca.pem -CAkey test/ca.key \
-CAcreateserial -outform PEM -out test/client_cert.pem \
-extensions v3_req -extfile test/client.cnf
# Create combined files
cat test/server_cert.pem test/server.key > test/server.pem
cat test/client_cert.pem test/client.key > test/client.pem

View File

@ -4,15 +4,15 @@
-module(proto_sup). -module(proto_sup).
-behaviour(supervisor). -behaviour(supervisor).
-export([start_link/1]). -export([start_link/2]).
-export([init/1]). -export([init/1]).
start_link(Port) -> start_link(Port, CertDir) ->
supervisor:start_link({local, ?MODULE}, ?MODULE, [Port]). supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir]).
init([Port]) -> init([Port, CertDir]) ->
SupFlags = #{stragegy => one_for_all, SupFlags = #{stragegy => one_for_all,
intensity => 1, intensity => 1,
period => 5}, period => 5},
ChildSpecs = [tcp_server:child_spec(Port)], ChildSpecs = [tcp_server:child_spec(Port, CertDir)],
{ok, {SupFlags, ChildSpecs}}. {ok, {SupFlags, ChildSpecs}}.

View File

@ -3,8 +3,8 @@
{vsn, "0.1.0"}, {vsn, "0.1.0"},
{registered, []}, {registered, []},
{mod, {study_system_server_app, []}}, {mod, {study_system_server_app, []}},
{applications, [kernel, stdlib]}, {applications, [kernel, stdlib, ssl]},
{env, []}, {env, [{cert_dir, "../test"}]},
{modules, []}, {modules, []},
{licenses, ["AGPL-3.0-only"]}, {licenses, ["AGPL-3.0-only"]},
{links, []}]}. {links, []}]}.

View File

@ -8,7 +8,8 @@
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
Port = application:get_env(study_system_server, port, 12888), Port = application:get_env(study_system_server, port, 12888),
proto_sup:start_link(Port). {ok, CertDir} = application:get_env(study_system_server, cert_dir),
proto_sup:start_link(Port, CertDir).
stop(_State) -> stop(_State) ->
ok. ok.

View File

@ -4,25 +4,31 @@
-module(tcp_server). -module(tcp_server).
-behaviour(gen_server). -behaviour(gen_server).
-export([start_link/1, child_spec/1]). -export([start_link/2, child_spec/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]). terminate/2, code_change/3]).
-record(state, {socket, acceptor}). -record(state, {socket, acceptor}).
start_link(Port) -> start_link(Port, CertDir) ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [Port], []). gen_server:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir], []).
child_spec(Port) -> child_spec(Port, CertDir) ->
#{id => ?MODULE, #{id => ?MODULE,
start => {?MODULE, start_link, [Port]}, start => {?MODULE, start_link, [Port, CertDir]},
restart => permanent, restart => permanent,
shutdown => 5000, shutdown => 5000,
type => worker, type => worker,
modules => [?MODULE]}. modules => [?MODULE]}.
init([Port]) -> init([Port, CertDir]) ->
{ok, Socket} = gen_tcp:listen(Port, [binary, inet6, {active, true}]), io:format("cert dir: ~p~n", [CertDir]),
SslOpts = [{certfile, filename:join([CertDir, "server.pem"])},
{cacertfile, filename:join([CertDir, "ca.pem"])},
{verify, verify_peer},
{fail_if_no_peer_cert, true}],
{ok, Socket} =
ssl:listen(Port, [binary, inet6, {active, true} | SslOpts]),
Pid = spawn_link(fun() -> acceptor_loop(Socket) end), Pid = spawn_link(fun() -> acceptor_loop(Socket) end),
{ok, #state{socket = Socket, acceptor = Pid}}. {ok, #state{socket = Socket, acceptor = Pid}}.
@ -45,21 +51,27 @@ code_change(_OldVsn, State, _Extra) ->
{ok, State}. {ok, State}.
acceptor_loop(Socket) -> acceptor_loop(Socket) ->
case gen_tcp:accept(Socket) of case ssl:transport_accept(Socket) of
{ok, ClientSocket} -> {ok, TlsSocket} ->
gen_tcp:controlling_process( case ssl:handshake(TlsSocket) of
ClientSocket, {ok, ClientSocket} ->
spawn(fun() -> handle_connection(ClientSocket) end)), Pid = spawn(
acceptor_loop(Socket); fun() -> handle_connection(ClientSocket) end),
ok = ssl:controlling_process(ClientSocket, Pid),
ssl:setopts(ClientSocket, [{active, true}]),
acceptor_loop(Socket);
{error, _Reason} ->
acceptor_loop(Socket)
end;
{error, closed} -> {error, closed} ->
ok ok
end. end.
handle_connection(Socket) -> handle_connection(Socket) ->
receive receive
{tcp, Socket, Data} -> {ssl, Socket, Data} ->
gen_tcp:send(Socket, Data), ssl:send(Socket, Data),
handle_connection(Socket); handle_connection(Socket);
{tcp_closed, Socket} -> {ssl_closed, Socket} ->
ok ok
end. end.