commit 02be96c6ddfc34e448cccd095b4f3d0efe4de8a3 from: Omar Polo date: Tue Feb 09 22:30:04 2021 UTC add `require client ca' rule to require certs signed by a CA 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 + + * parse.y (locopt): add `require client ca' rule to require client certs signed by a specified CA + 2021-02-07 Omar Polo * 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 #include +#include + #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 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 # generates a configuration file reg.conf config() { @@ -25,19 +27,19 @@ checkconf() { # usage: get # return the body of the request on stdout get() { - ./../gg -b "gemini://localhost:10965/$1" + ./../gg -b $ggflags "gemini://localhost:10965/$1" } # usage: head # return the meta response line on stdout head() { - ./../gg -h "gemini://localhost:10965/$1" + ./../gg -h $ggflags "gemini://localhost:10965/$1" } # usage: raw # 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 #include -#include +#include +#include #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; +}