From ebf9afb4e1bf281f9748404dac070b26e2ab4b8a Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Sun, 23 Feb 2025 01:32:59 +0000 Subject: [PATCH] Implement mTLS authentication between client and server --- .gitignore | 1 + client/config.vapi | 4 ++ client/main.vala | 53 ++++++++++++------ client/meson.build | 14 ++++- scripts/make-test-certs.sh | 76 ++++++++++++++++++++++++++ server/src/proto_sup.erl | 10 ++-- server/src/study_system_server.app.src | 4 +- server/src/study_system_server_app.erl | 3 +- server/src/tcp_server.erl | 44 +++++++++------ 9 files changed, 166 insertions(+), 43 deletions(-) create mode 100644 .gitignore create mode 100644 client/config.vapi create mode 100755 scripts/make-test-certs.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab1cfb4 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +test/* diff --git a/client/config.vapi b/client/config.vapi new file mode 100644 index 0000000..13952a8 --- /dev/null +++ b/client/config.vapi @@ -0,0 +1,4 @@ +[CCode (cheader_filename = "config.h")] +namespace Config { + public const string CERT_DIR; +} diff --git a/client/main.vala b/client/main.vala index f09088b..bb76b93 100644 --- a/client/main.vala +++ b/client/main.vala @@ -1,29 +1,41 @@ using Gtk; public class Connection { - private InetSocketAddress host; public signal void response_received(uint8[] response); - public Connection() { + private TlsClientConnection tls_client; + + public Connection() throws Error { 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 { - var client = new SocketClient(); - var conn = yield client.connect_async(host); - yield conn.output_stream.write_async(message); - + yield tls_client.output_stream.write_async(message); 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]); } } public class MainWindow : Gtk.ApplicationWindow { private Connection connection; + private Gtk.Button send_button; private Gtk.Label response_label; - private Gtk.Button connect_button; public MainWindow(Gtk.Application app, Connection connection) { Object(application: app); @@ -35,7 +47,7 @@ public class MainWindow : Gtk.ApplicationWindow { this.connection = connection; connection.response_received.connect((response) => { response_label.label = "Response: " + (string)response; - }); + }); var box = new Gtk.Box(Gtk.Orientation.VERTICAL, 10); box.margin_start = 10; @@ -43,22 +55,22 @@ public class MainWindow : Gtk.ApplicationWindow { box.margin_top = 10; box.margin_bottom = 10; - connect_button = new Gtk.Button.with_label("Foo"); - connect_button.clicked.connect(on_connect_clicked); - box.append(connect_button); + send_button = new Gtk.Button.with_label("Send"); + send_button.clicked.connect(on_send_clicked); + box.append(send_button); response_label = new Gtk.Label(""); response_label.wrap = true; box.append(response_label); set_child(box); - + present(); } - private async void on_connect_clicked() { + private async void on_send_clicked() { try { - yield connection.send("Bar".data); + yield connection.send("Foo".data); } catch (Error e) { response_label.label = "Error: " + e.message; } @@ -71,8 +83,13 @@ public class StudySystemClient : Gtk.Application { } protected override void activate() { - var connection = new Connection(); - new MainWindow(this, connection); + try { + 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) { diff --git a/client/meson.build b/client/meson.build index 8aa5328..07ad422 100644 --- a/client/meson.build +++ b/client/meson.build @@ -10,9 +10,21 @@ add_project_arguments('-w', language: 'c') 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( 'study-system-client', - 'main.vala', + [ + 'main.vala', + 'config.vapi' + ], dependencies: [gtk_dep], c_args: ['-w'] ) diff --git a/scripts/make-test-certs.sh b/scripts/make-test-certs.sh new file mode 100755 index 0000000..15fc710 --- /dev/null +++ b/scripts/make-test-certs.sh @@ -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 diff --git a/server/src/proto_sup.erl b/server/src/proto_sup.erl index c7d5db6..b2a2e12 100644 --- a/server/src/proto_sup.erl +++ b/server/src/proto_sup.erl @@ -4,15 +4,15 @@ -module(proto_sup). -behaviour(supervisor). --export([start_link/1]). +-export([start_link/2]). -export([init/1]). -start_link(Port) -> - supervisor:start_link({local, ?MODULE}, ?MODULE, [Port]). +start_link(Port, CertDir) -> + supervisor:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir]). -init([Port]) -> +init([Port, CertDir]) -> SupFlags = #{stragegy => one_for_all, intensity => 1, period => 5}, - ChildSpecs = [tcp_server:child_spec(Port)], + ChildSpecs = [tcp_server:child_spec(Port, CertDir)], {ok, {SupFlags, ChildSpecs}}. diff --git a/server/src/study_system_server.app.src b/server/src/study_system_server.app.src index 73c2d2f..9ebb138 100644 --- a/server/src/study_system_server.app.src +++ b/server/src/study_system_server.app.src @@ -3,8 +3,8 @@ {vsn, "0.1.0"}, {registered, []}, {mod, {study_system_server_app, []}}, - {applications, [kernel, stdlib]}, - {env, []}, + {applications, [kernel, stdlib, ssl]}, + {env, [{cert_dir, "../test"}]}, {modules, []}, {licenses, ["AGPL-3.0-only"]}, {links, []}]}. diff --git a/server/src/study_system_server_app.erl b/server/src/study_system_server_app.erl index 095435a..5f0e104 100644 --- a/server/src/study_system_server_app.erl +++ b/server/src/study_system_server_app.erl @@ -8,7 +8,8 @@ start(_StartType, _StartArgs) -> 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) -> ok. diff --git a/server/src/tcp_server.erl b/server/src/tcp_server.erl index e1e0da6..1c071f2 100644 --- a/server/src/tcp_server.erl +++ b/server/src/tcp_server.erl @@ -4,25 +4,31 @@ -module(tcp_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, terminate/2, code_change/3]). -record(state, {socket, acceptor}). -start_link(Port) -> - gen_server:start_link({local, ?MODULE}, ?MODULE, [Port], []). +start_link(Port, CertDir) -> + gen_server:start_link({local, ?MODULE}, ?MODULE, [Port, CertDir], []). -child_spec(Port) -> +child_spec(Port, CertDir) -> #{id => ?MODULE, - start => {?MODULE, start_link, [Port]}, + start => {?MODULE, start_link, [Port, CertDir]}, restart => permanent, shutdown => 5000, type => worker, modules => [?MODULE]}. -init([Port]) -> - {ok, Socket} = gen_tcp:listen(Port, [binary, inet6, {active, true}]), +init([Port, CertDir]) -> + 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), {ok, #state{socket = Socket, acceptor = Pid}}. @@ -45,21 +51,27 @@ code_change(_OldVsn, State, _Extra) -> {ok, State}. acceptor_loop(Socket) -> - case gen_tcp:accept(Socket) of - {ok, ClientSocket} -> - gen_tcp:controlling_process( - ClientSocket, - spawn(fun() -> handle_connection(ClientSocket) end)), - acceptor_loop(Socket); + case ssl:transport_accept(Socket) of + {ok, TlsSocket} -> + case ssl:handshake(TlsSocket) of + {ok, ClientSocket} -> + Pid = spawn( + 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} -> ok end. handle_connection(Socket) -> receive - {tcp, Socket, Data} -> - gen_tcp:send(Socket, Data), + {ssl, Socket, Data} -> + ssl:send(Socket, Data), handle_connection(Socket); - {tcp_closed, Socket} -> + {ssl_closed, Socket} -> ok end.