Implement mTLS authentication between client and server
This commit is contained in:
parent
83ab6f7a20
commit
ebf9afb4e1
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
test/*
|
4
client/config.vapi
Normal file
4
client/config.vapi
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
[CCode (cheader_filename = "config.h")]
|
||||||
|
namespace Config {
|
||||||
|
public const string CERT_DIR;
|
||||||
|
}
|
@ -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) {
|
||||||
|
@ -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
76
scripts/make-test-certs.sh
Executable 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
|
@ -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}}.
|
||||||
|
@ -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, []}]}.
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user