Commit Diff


commit - 2ff026b09b810efd8c52e13f0a4988c588c8ee09
commit + 02be96c6ddfc34e448cccd095b4f3d0efe4de8a3
blob - 88532ad99a042f80f134958d8f91850bc1007084
blob + 936f164a5a5bfad0ec82a3de8fc56df6d580ed9d
--- .gitignore
+++ .gitignore
@@ -15,6 +15,10 @@ config.log.old
 configure.local
 regress/testdata
 regress/*.pem
+regress/*.key
+regress/*.crt
+regress/*.csr
+regress/*.srl
 regress/reg.conf
 regress/fill-file
 regress/iri_test
blob - 1f9865329807229011b4d195234cf2f3d8985fd0
blob + 57b7a598ddeea27590c0a34be85654ae6ee30183
--- ChangeLog
+++ ChangeLog
@@ -1,3 +1,7 @@
+2021-02-09  Omar Polo  <op@omarpolo.com>
+
+	* parse.y (locopt): add `require client ca' rule to require client certs signed by a specified CA
+
 2021-02-07  Omar Polo  <op@omarpolo.com>
 
 	* ex.c (do_exec): [cgi] split the query in words if needed and add them to the argv
blob - faf3e4fb8c3accc0c4c5af162be196b51755286e
blob + daa8a30114c2cb7b84a4bb5d3a1a862b4898cf8e
--- gmid.1
+++ gmid.1
@@ -276,6 +276,12 @@ except
 Specify the root directory for this server.
 This option is mandatory.
 It's relative to the chroot, if enabled.
+.It Ic require Ic client Ic ca Pa path
+Allow requests only from clients that provide a certificate signed by
+the CA certificate in
+.Pa path .
+It needs to be a PEM-encoded certificate and it's not relative to the
+chroot.
 .It Ic strip Ar number
 Strip
 .Ar number
blob - b031d51d91fe2d352e1c6df5b4353f397a3842da
blob + 2fdfc7e8a09113d36dbbe9b35909384cdd5f4a53
--- gmid.h
+++ gmid.h
@@ -31,6 +31,8 @@
 #include <tls.h>
 #include <unistd.h>
 
+#include <openssl/x509.h>
+
 #include "config.h"
 
 #define GEMINI_URL_LEN (1024+3)	/* URL max len + \r\n + \0 */
@@ -42,6 +44,8 @@
 #define NOT_FOUND	51
 #define PROXY_REFUSED	53
 #define BAD_REQUEST	59
+#define CLIENT_CERT_REQ	60
+#define CERT_NOT_AUTH	61
 
 #define MAX_USERS	64
 
@@ -61,6 +65,7 @@ struct location {
 	int		 block_code;
 	const char	*block_fmt;
 	int		 strip;
+	X509_STORE	*reqca;
 };
 
 struct vhost {
@@ -228,6 +233,7 @@ const char	*vhost_index(struct vhost*, const char*);
 int		 vhost_auto_index(struct vhost*, const char*);
 int		 vhost_block_return(struct vhost*, const char*, int*, const char**);
 int		 vhost_strip(struct vhost*, const char*);
+X509_STORE	*vhost_require_ca(struct vhost*, const char*);
 void		 mark_nonblock(int);
 void		 loop(struct tls*, int, int);
 
@@ -270,5 +276,7 @@ ssize_t		 filesize(int);
 char		*absolutify_path(const char*);
 char		*xstrdup(const char*);
 void		 gen_certificate(const char*, const char*, const char*);
+X509_STORE	*load_ca(const char*);
+int		 validate_against_ca(X509_STORE*, const uint8_t*, size_t);
 
 #endif
blob - abfced4b7cf6eb8c4dde49c5c6d69a5fb3bdc489
blob + 65f8acccf4ef9938ccc7ea2191a03cf939f678ee
--- lex.l
+++ lex.l
@@ -74,6 +74,9 @@ strip		return TSTRIP;
 block		return TBLOCK;
 return		return TRETURN;
 entrypoint	return TENTRYPOINT;
+require		return TREQUIRE;
+client		return TCLIENT;
+ca		return TCA;
 
 [{}]		return *yytext;
 
blob - ab7a3e812b2807f8778587f230b36ecca1cad4a0
blob + 64c787944f3c88d5958b707866050d01aeb21040
--- parse.y
+++ parse.y
@@ -58,7 +58,7 @@ int		 check_prefork_num(int);
 %token TIPV6 TPORT TPROTOCOLS TMIME TDEFAULT TTYPE
 %token TCHROOT TUSER TSERVER TPREFORK
 %token TLOCATION TCERT TKEY TROOT TCGI TLANG TINDEX TAUTO
-%token TSTRIP TBLOCK TRETURN TENTRYPOINT
+%token TSTRIP TBLOCK TRETURN TENTRYPOINT TREQUIRE TCLIENT TCA
 %token TERR
 
 %token <str>	TSTRING
@@ -190,6 +190,15 @@ locopt		: TDEFAULT TTYPE TSTRING {
 			loc->block_code = 40;
 		}
 		| TSTRIP TNUM		{ loc->strip = check_strip_no($2); }
+		| TREQUIRE TCLIENT TCA TSTRING {
+			if (loc->reqca != NULL)
+				yyerror("`require client ca' specified more than once");
+			if (*$4 != '/')
+				yyerror("path to certificate must be absolute: %s", $4);
+			if ((loc->reqca = load_ca($4)) == NULL)
+				yyerror("couldn't load ca cert: %s", $4);
+			free($4);
+		}
 		;
 
 %%
blob - 56f3d5567eb4c7251a812f3f20ee4776eea236b9
blob + e29e94c940d9be77d4bf620e5cd88cf527fa1d1e
--- regress/Makefile
+++ regress/Makefile
@@ -2,7 +2,7 @@ include ../Makefile.local
 
 .PHONY: all clean runtime
 
-all: puny-test testdata iri_test cert.pem
+all: puny-test testdata iri_test cert.pem testca.pem valid.crt invalid.cert.pem
 	./puny-test
 	./runtime
 	./iri_test
@@ -28,9 +28,38 @@ cert.pem:
 			-days 365 -nodes
 	@echo
 
+testca.pem:
+	openssl genrsa -out testca.key 2048
+	printf ".\n.\n.\n.\n.\ntestca\n.\n" |	\
+		openssl req -x509 -new -sha256		\
+			-key testca.key			\
+			-out cert.pem			\
+			-days 365 -nodes		\
+			-out testca.pem
+	@echo
+
+valid.crt: testca.pem
+	openssl genrsa -out valid.key 2048
+	printf ".\n.\n.\n.\n.\nvalid\n.\n\n" |	\
+		openssl req -new -key valid.key	\
+			-out valid.csr
+	@echo
+	openssl x509 -req -in valid.csr		\
+		-CA testca.pem			\
+		-CAkey testca.key		\
+		-CAcreateserial			\
+		-out valid.crt			\
+		-days 365			\
+		-sha256 -extfile valid.ext
+
+invalid.cert.pem: cert.pem
+	cp cert.pem invalid.cert.pem
+	cp key.pem invalid.key.pem
+
 clean:
 	rm -f *.o iri_test cert.pem key.pem
-	rm -rf testdata
+	rm -f testca.* valid.* invalid.*pem
+	rm -rf testdata fill-file puny-test
 
 testdata: fill-file
 	mkdir testdata
blob - 1c991b3530bd7f9d63f0b5bedc5690ea41de564a
blob + a05184a5216b77d8bec05239db625a3121ec1c97
--- regress/runtime
+++ regress/runtime
@@ -2,6 +2,8 @@
 
 set -e
 
+ggflags=
+
 # usage: config <global config> <stuff for localhost>
 # generates a configuration file reg.conf
 config() {
@@ -25,19 +27,19 @@ checkconf() {
 # usage: get <path>
 # return the body of the request on stdout
 get() {
-	./../gg -b "gemini://localhost:10965/$1"
+	./../gg -b $ggflags "gemini://localhost:10965/$1"
 }
 
 # usage: head <path>
 # return the meta response line on stdout
 head() {
-	./../gg -h "gemini://localhost:10965/$1"
+	./../gg -h $ggflags "gemini://localhost:10965/$1"
 }
 
 # usage: raw <path>
 # return both header and body
 raw() {
-	./../gg "gemini://localhost:10965/$1"
+	./../gg $ggflags "gemini://localhost:10965/$1"
 }
 
 run() {
@@ -276,4 +278,23 @@ eq "$(head /foo/bar)"	"20 text/plain; lang=en"	"Unknow
 eq "$(get /foo/bar|grep PATH_INFO)" "PATH_INFO=/foo/bar" "Unexpected PATH_INFO"
 echo OK GET /foo/bar with entrypoint
 
+# test with require ca
+
+config '' 'require client ca "'$PWD'/testca.pem"'
+checkconf
+restart
+
+eq "$(head /)"		"60 client certificate required" "Unexpected head for /"
+echo OK GET / without client certificate
+
+ggflags="-C valid.crt -K valid.key"
+eq "$(head /)"		"20 text/gemini"		"Unexpected head for /"
+echo OK GET / with valid client certificate
+
+ggflags="-C invalid.cert.pem -K invalid.key.pem"
+eq "$(head /)"		"61 certificate not authorised"	"Unexpected head for /"
+echo OK GET / with invalid client certificate
+
+ggflags=''
+
 quit
blob - 6626648773e812b66512ec4ea8c1c8520cdc4ef9
blob + 1dc0d9102a9d818b854019e1d7eb036f3a1d0d99
--- server.c
+++ server.c
@@ -54,6 +54,7 @@ static void	 handle_handshake(int, short, void*);
 static char	*strip_path(char*, int);
 static void	 fmt_sbuf(const char*, struct client*, const char*);
 static int	 apply_block_return(struct client*);
+static int	 apply_require_ca(struct client*);
 static void	 handle_open_conn(int, short, void*);
 static void	 start_reply(struct client*, int, const char*);
 static void	 handle_start_reply(int, short, void*);
@@ -202,6 +203,24 @@ vhost_strip(struct vhost *v, const char *path)
 	return v->locations[0].strip;
 }
 
+X509_STORE *
+vhost_require_ca(struct vhost *v, const char *path)
+{
+	struct location *loc;
+
+	if (v == NULL || path == NULL)
+		return NULL;
+
+	for (loc = &v->locations[1]; loc->match != NULL; ++loc) {
+		if (loc->reqca != NULL) {
+			if (!fnmatch(loc->match, path, 0))
+				return loc->reqca;
+		}
+	}
+
+	return v->locations[0].reqca;
+}
+
 static int
 check_path(struct client *c, const char *path, int *fd)
 {
@@ -483,6 +502,31 @@ apply_block_return(struct client *c)
 	return 1;
 }
 
+/* 1 if matching `require client ca' fails (and apply it), 0 otherwise */
+static int
+apply_require_ca(struct client *c)
+{
+	X509_STORE	*store;
+	const uint8_t	*cert;
+	size_t		 len;
+
+	if ((store = vhost_require_ca(c->host, c->iri.path)) == NULL)
+		return 0;
+
+	if (!tls_peer_cert_provided(c->ctx)) {
+		start_reply(c, CLIENT_CERT_REQ, "client certificate required");
+		return 1;
+	}
+
+	cert = tls_peer_cert_chain_pem(c->ctx, &len);
+	if (!validate_against_ca(store, cert, len)) {
+		start_reply(c, CERT_NOT_AUTH, "certificate not authorised");
+		return 1;
+	}
+
+	return 0;
+}
+
 static void
 handle_open_conn(int fd, short ev, void *d)
 {
@@ -523,6 +567,9 @@ handle_open_conn(int fd, short ev, void *d)
 		return;
 	}
 
+	if (apply_require_ca(c))
+		return;
+
 	if (apply_block_return(c))
 		return;
 
blob - bd24b84af161f81fcda1ff105f58b1c2334173fa
blob + 66c7ced24a7b4af27b971aa69402bc72d33239a6
--- utils.c
+++ utils.c
@@ -18,7 +18,8 @@
 #include <string.h>
 
 #include <openssl/pem.h>
-#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+#include <openssl/x509v3.h>
 
 #include "gmid.h"
 
@@ -176,3 +177,70 @@ gen_certificate(const char *host, const char *certpath
 	X509_free(x509);
 	RSA_free(rsa);
 }
+
+X509_STORE *
+load_ca(const char *path)
+{
+	FILE		*f = NULL;
+	X509		*x = NULL;
+	X509_STORE	*store;
+
+	if ((store = X509_STORE_new()) == NULL)
+		return NULL;
+
+	if ((f = fopen(path, "r")) == NULL)
+		goto err;
+
+	if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL)
+		goto err;
+
+	if (X509_check_ca(x) == 0)
+		goto err;
+
+	if (!X509_STORE_add_cert(store, x))
+		goto err;
+
+	X509_free(x);
+	fclose(f);
+	return store;
+
+err:
+	X509_STORE_free(store);
+	if (x != NULL)
+		X509_free(x);
+	if (f != NULL)
+		fclose(f);
+	return NULL;
+}
+
+int
+validate_against_ca(X509_STORE *ca, const uint8_t *chain, size_t len)
+{
+	X509		*client;
+	BIO		*m;
+	X509_STORE_CTX	*ctx = NULL;
+	int		 ret = 0;
+
+	if ((m = BIO_new_mem_buf(chain, len)) == NULL)
+		return 0;
+
+	if ((client = PEM_read_bio_X509(m, NULL, NULL, NULL)) == NULL)
+		goto end;
+
+	if ((ctx = X509_STORE_CTX_new()) == NULL)
+		goto end;
+
+	if (!X509_STORE_CTX_init(ctx, ca, client, NULL))
+		goto end;
+
+	ret = X509_verify_cert(ctx);
+	fprintf(stderr, "openssl x509_verify_cert: %d\n", ret);
+
+end:
+	BIO_free(m);
+	if (client != NULL)
+		X509_free(client);
+	if (ctx != NULL)
+		X509_STORE_CTX_free(ctx);
+	return ret;
+}