add `require client ca' rule to require certs signed by a CA

This commit is contained in:
Omar Polo 2021-02-09 22:30:04 +00:00
parent 2ff026b09b
commit 02be96c6dd
10 changed files with 206 additions and 7 deletions

4
.gitignore vendored
View File

@ -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

View File

@ -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

6
gmid.1
View File

@ -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

8
gmid.h
View File

@ -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

3
lex.l
View File

@ -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;

11
parse.y
View File

@ -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);
}
;
%%

View File

@ -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

View File

@ -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" "Unknown head for /foo/bar"
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

View File

@ -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;

70
utils.c
View File

@ -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, const char *keypath)
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;
}