mirror of https://github.com/omar-polo/gmid.git
add `require client ca' rule to require certs signed by a CA
This commit is contained in:
parent
2ff026b09b
commit
02be96c6dd
|
@ -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
|
||||
|
|
|
@ -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
6
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
|
||||
|
|
8
gmid.h
8
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
|
||||
|
|
3
lex.l
3
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;
|
||||
|
||||
|
|
11
parse.y
11
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);
|
||||
}
|
||||
;
|
||||
|
||||
%%
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
47
server.c
47
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;
|
||||
|
||||
|
|
70
utils.c
70
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, 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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue