commit - 2ff026b09b810efd8c52e13f0a4988c588c8ee09
commit + 02be96c6ddfc34e448cccd095b4f3d0efe4de8a3
blob - 88532ad99a042f80f134958d8f91850bc1007084
blob + 936f164a5a5bfad0ec82a3de8fc56df6d580ed9d
--- .gitignore
+++ .gitignore
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
+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
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
#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 */
#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
int block_code;
const char *block_fmt;
int strip;
+ X509_STORE *reqca;
};
struct vhost {
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);
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
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
%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
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
.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
-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
set -e
+ggflags=
+
# usage: config <global config> <stuff for localhost>
# generates a configuration file reg.conf
config() {
# 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() {
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
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*);
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)
{
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)
{
return;
}
+ if (apply_require_ca(c))
+ return;
+
if (apply_block_return(c))
return;
blob - bd24b84af161f81fcda1ff105f58b1c2334173fa
blob + 66c7ced24a7b4af27b971aa69402bc72d33239a6
--- utils.c
+++ utils.c
#include <string.h>
#include <openssl/pem.h>
-#include <openssl/x509.h>
+#include <openssl/x509_vfy.h>
+#include <openssl/x509v3.h>
#include "gmid.h"
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;
+}