diff --git a/configure b/configure
index 70a19b193d6e33b41b5cc7ef13511bb0f5b4e076..7b55a2580018cc69b2f52c9bc53832cc9faadc4f 100755
--- a/configure
+++ b/configure
@@ -4,6 +4,7 @@ eval ". $srcdir/config.sh"
gmni() {
genrules gmni \
+ src/certs.c \
src/client.c \
src/escape.c \
src/gmni.c \
diff --git a/doc/gmni.scd b/doc/gmni.scd
index 1f20672f1fb0c8ff6a4f3a97a3324a25d2a0a01d..866dd418a510ccd141d9c23afb4b321b84a5f9ec 100644
--- a/doc/gmni.scd
+++ b/doc/gmni.scd
@@ -38,11 +38,9 @@ If the server requests user input, _path_ is opened and read, and a
second request is performed with the contents of _path_ as the user
input.
-*-E* _path_[:_password_]
- Sets the path to the client certificate to use (and optionally a
- password). If the filename contains ":" but the certificate does not
- accept a password, append ":" to the path and it will be intepreted as
- an empty password.
+*-E* _path_:_key_
+ Sets the path to the client certificate and private key file to use,
+ both PEM encoded.
*-l*
For *text/\** responses, *gmni* normally adds a line feed if stdout is a
diff --git a/include/gmni/certs.h b/include/gmni/certs.h
new file mode 100644
index 0000000000000000000000000000000000000000..22e226d6b4252dddd8526970cec0a947f12242d1
--- /dev/null
+++ b/include/gmni/certs.h
@@ -0,0 +1,27 @@
+#ifndef GEMINI_CERTS_H
+#define GEMINI_CERTS_H
+#include <bearssl.h>
+#include <stdio.h>
+
+struct gmni_options;
+
+struct gmni_client_certificate {
+ br_x509_certificate *chain;
+ size_t nchain;
+ struct gmni_private_key *key;
+};
+
+struct gmni_private_key {
+ int type;
+ union {
+ br_rsa_private_key rsa;
+ br_ec_private_key ec;
+ };
+ unsigned char data[];
+};
+
+// Returns nonzero on failure and sets errno
+int gmni_ccert_load(struct gmni_client_certificate *cert,
+ FILE *certin, FILE *skin);
+
+#endif
diff --git a/include/gmni/gmni.h b/include/gmni/gmni.h
index 16bef51024275bbb6ade9c90a11ae68876df387a..22295e20fb36d492a979c060de8dcdca14df5a34 100644
--- a/include/gmni/gmni.h
+++ b/include/gmni/gmni.h
@@ -1,6 +1,6 @@
#ifndef GEMINI_CLIENT_H
#define GEMINI_CLIENT_H
-#include <bearssl_ssl.h>
+#include <bearssl.h>
#include <netdb.h>
#include <stdbool.h>
#include <sys/socket.h>
@@ -61,6 +61,8 @@ br_ssl_client_context *sc;
int fd;
};
+struct gmni_client_certificate;
+
struct gemini_options {
// If ai_family != AF_UNSPEC (the default value on most systems), the
// client will connect to this address and skip name resolution.
@@ -69,6 +71,10 @@
// If non-NULL, these hints are provided to getaddrinfo. Useful, for
// example, to force IPv4/IPv6.
struct addrinfo *hints;
+
+ // If non-NULL, this will be used as the client certificate for the
+ // request. The other fields must be set as well.
+ struct gmni_client_certificate *client_cert;
};
struct gemini_tofu;
diff --git a/include/gmni/tofu.h b/include/gmni/tofu.h
index a0981a5296421541a766846bc36c733dab1dfd1a..51d1d60a3719469a1f8cd295b9d85e21d7affb43 100644
--- a/include/gmni/tofu.h
+++ b/include/gmni/tofu.h
@@ -1,6 +1,6 @@
#ifndef GEMINI_TOFU_H
#define GEMINI_TOFU_H
-#include <bearssl_x509.h>
+#include <bearssl.h>
#include <limits.h>
enum tofu_error {
diff --git a/src/certs.c b/src/certs.c
new file mode 100644
index 0000000000000000000000000000000000000000..f40bfa7d27925baaa2fe6dbdf0d6c18ce4e10014
--- /dev/null
+++ b/src/certs.c
@@ -0,0 +1,156 @@
+#include <assert.h>
+#include <bearssl.h>
+#include <errno.h>
+#include <gmni/certs.h>
+#include <gmni/gmni.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+static void
+crt_append(void *ctx, const void *src, size_t len)
+{
+ br_x509_certificate *crt = (br_x509_certificate *)ctx;
+ crt->data = realloc(crt->data, crt->data_len + len);
+ assert(crt->data);
+ memcpy(&crt->data[crt->data_len], src, len);
+ crt->data_len += len;
+}
+
+static void
+key_append(void *ctx, const void *src, size_t len)
+{
+ br_skey_decoder_context *skctx = (br_skey_decoder_context *)ctx;
+ br_skey_decoder_push(skctx, src, len);
+}
+
+int
+gmni_ccert_load(struct gmni_client_certificate *cert, FILE *certin, FILE *skin)
+{
+ // TODO: Better error propagation to caller
+ static unsigned char buf[BUFSIZ];
+
+ br_pem_decoder_context pemdec;
+ br_pem_decoder_init(&pemdec);
+
+ cert->chain = NULL;
+ cert->nchain = 0;
+
+ static const char *certname = "CERTIFICATE";
+ while (!feof(certin)) {
+ size_t n = fread(&buf, 1, sizeof(buf), certin);
+ if (ferror(certin)) {
+ goto error;
+ }
+ size_t q = 0;
+ while (q < n) {
+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q);
+ switch (br_pem_decoder_event(&pemdec)) {
+ case BR_PEM_BEGIN_OBJ:
+ if (strcmp(br_pem_decoder_name(&pemdec), certname) != 0) {
+ break;
+ }
+ cert->chain = realloc(cert->chain,
+ sizeof(br_x509_certificate) * (cert->nchain + 1));
+ memset(&cert->chain[cert->nchain], 0, sizeof(*cert->chain));
+ br_pem_decoder_setdest(&pemdec, &crt_append,
+ &cert->chain[cert->nchain]);
+ ++cert->nchain;
+ break;
+ case BR_PEM_END_OBJ:
+ break;
+ case BR_PEM_ERROR:
+ fprintf(stderr, "Error decoding PEM certificate\n");
+ errno = EINVAL;
+ goto error;
+ }
+ }
+ }
+
+ if (cert->nchain == 0) {
+ fprintf(stderr, "No certificates found in provided client certificate file\n");
+ errno = EINVAL;
+ goto error;
+ }
+
+ br_skey_decoder_context skdec = {0};
+ br_skey_decoder_init(&skdec);
+ br_pem_decoder_init(&pemdec);
+
+ // TODO: Better validation of PEM file
+ while (!feof(skin)) {
+ size_t n = fread(&buf, 1, sizeof(buf), skin);
+ if (ferror(skin)) {
+ goto error;
+ }
+ size_t q = 0;
+ while (q < n) {
+ q += br_pem_decoder_push(&pemdec, &buf[q], n - q);
+ switch (br_pem_decoder_event(&pemdec)) {
+ case BR_PEM_BEGIN_OBJ:
+ br_pem_decoder_setdest(&pemdec, &key_append, &skdec);
+ break;
+ case BR_PEM_END_OBJ:
+ // no-op
+ break;
+ case BR_PEM_ERROR:
+ fprintf(stderr, "Error decoding PEM private key\n");
+ errno = EINVAL;
+ goto error;
+ }
+ }
+ }
+
+ int err = br_skey_decoder_last_error(&skdec);
+ if (err != 0) {
+ fprintf(stderr, "Error loading private key: %d\n", err);
+ errno = EINVAL;
+ goto error;
+ }
+ switch (br_skey_decoder_key_type(&skdec)) {
+ struct gmni_private_key *k;
+ const br_ec_private_key *ec;
+ const br_rsa_private_key *rsa;
+ case BR_KEYTYPE_RSA:
+ rsa = br_skey_decoder_get_rsa(&skdec);
+ cert->key = k = malloc(sizeof(*k)
+ + rsa->plen + rsa->qlen
+ + rsa->dplen + rsa->dqlen
+ + rsa->iqlen);
+ assert(k);
+ k->type = BR_KEYTYPE_RSA;
+ k->rsa = *rsa;
+ k->rsa.p = k->data;
+ k->rsa.q = k->rsa.p + k->rsa.plen;
+ k->rsa.dp = k->rsa.q + k->rsa.qlen;
+ k->rsa.dq = k->rsa.dp + k->rsa.dplen;
+ k->rsa.iq = k->rsa.dq + k->rsa.dqlen;
+ memcpy(k->rsa.p, rsa->p, rsa->plen);
+ memcpy(k->rsa.q, rsa->q, rsa->qlen);
+ memcpy(k->rsa.dp, rsa->dp, rsa->dplen);
+ memcpy(k->rsa.dq, rsa->dq, rsa->dqlen);
+ memcpy(k->rsa.iq, rsa->iq, rsa->iqlen);
+ break;
+ case BR_KEYTYPE_EC:
+ ec = br_skey_decoder_get_ec(&skdec);
+ cert->key = k = malloc(sizeof(*k) + ec->xlen);
+ assert(k);
+ k->type = BR_KEYTYPE_EC;
+ k->ec.curve = ec->curve;
+ k->ec.x = k->data;
+ k->ec.xlen = ec->xlen;
+ memcpy(k->ec.x, ec->x, ec->xlen);
+ break;
+ default:
+ assert(0);
+ }
+
+ fclose(certin);
+ fclose(skin);
+ return 0;
+
+error:
+ fclose(certin);
+ fclose(skin);
+ free(cert->chain);
+ return 1;
+}
diff --git a/src/client.c b/src/client.c
index e402cc97d96a904a2a8e9e2db40345b222cb85ae..127a56ca59859e645fea6f1e4ac62431d734e4fd 100644
--- a/src/client.c
+++ b/src/client.c
@@ -1,13 +1,14 @@
#include <assert.h>
#include <errno.h>
#include <netdb.h>
-#include <bearssl_ssl.h>
+#include <bearssl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
+#include <gmni/certs.h>
#include <gmni/gmni.h>
#include <gmni/tofu.h>
#include <gmni/url.h>
@@ -169,7 +170,26 @@ }
// TODO: session reuse
resp->sc = &tofu->sc;
+ if (options->client_cert) {
+ struct gmni_client_certificate *cert = options->client_cert;
+ struct gmni_private_key *key = cert->key;
+ switch (key->type) {
+ case BR_KEYTYPE_RSA:
+ br_ssl_client_set_single_rsa(resp->sc,
+ cert->chain, cert->nchain, &key->rsa,
+ br_rsa_pkcs1_sign_get_default());
+ break;
+ case BR_KEYTYPE_EC:
+ br_ssl_client_set_single_ec(resp->sc,
+ cert->chain, cert->nchain, &key->ec,
+ BR_KEYTYPE_SIGN, 0,
+ br_ec_get_default(),
+ br_ecdsa_sign_asn1_get_default());
+ break;
+ }
+ }
br_ssl_client_reset(resp->sc, host, 0);
+
br_sslio_init(&resp->body, &resp->sc->eng,
sock_read, &resp->fd, sock_write, &resp->fd);
diff --git a/src/gmni.c b/src/gmni.c
index a8321d06c367128706d19ac283a53e55d95d0a92..f3015ac679eba77397fb4aac5068f3a63e1cdbab 100644
--- a/src/gmni.c
+++ b/src/gmni.c
@@ -1,5 +1,5 @@
#include <assert.h>
-#include <bearssl_ssl.h>
+#include <bearssl.h>
#include <errno.h>
#include <getopt.h>
#include <netdb.h>
@@ -11,6 +11,7 @@ #include <sys/socket.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
+#include <gmni/certs.h>
#include <gmni/gmni.h>
#include <gmni/tofu.h>
#include <gmni/url.h>
@@ -109,6 +110,45 @@
return action;
}
+static struct gmni_client_certificate *
+load_client_cert(char *argv_0, char *path)
+{
+ char *certpath = strtok(path, ":");
+ if (!certpath) {
+ usage(argv_0);
+ exit(1);
+ }
+
+ FILE *certf = fopen(certpath, "r");
+ if (!certf) {
+ fprintf(stderr, "Failed to open certificate: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ char *keypath = strtok(NULL, ":");
+ if (!keypath) {
+ usage(argv_0);
+ exit(1);
+ }
+
+ FILE *keyf = fopen(keypath, "r");
+ if (!keyf) {
+ fprintf(stderr, "Failed to open certificate: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ struct gmni_client_certificate *cert =
+ calloc(1, sizeof(struct gmni_client_certificate));
+ if (gmni_ccert_load(cert, certf, keyf) != 0) {
+ fprintf(stderr, "Failed to load client certificate: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+ return cert;
+}
+
int
main(int argc, char *argv[])
{
@@ -165,7 +205,7 @@ }
}
break;
case 'E':
- assert(0); // TODO: Client certificates
+ opts.client_cert = load_client_cert(argv[0], optarg);
break;
case 'h':
usage(argv[0]);
@@ -226,7 +266,7 @@
bool exit = false;
struct Curl_URL *url = curl_url();
- if(curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) {
+ if (curl_url_set(url, CURLUPART_URL, argv[optind], 0) != CURLUE_OK) {
// TODO: Better error
fprintf(stderr, "Error: invalid URL\n");
return 1;
@@ -238,8 +278,8 @@ char *buf;
curl_url_get(url, CURLUPART_URL, &buf, 0);
struct gemini_response resp;
- enum gemini_result r = gemini_request(
- buf, &opts, &cfg.tofu, &resp);
+ enum gemini_result r = gemini_request(buf,
+ &opts, &cfg.tofu, &resp);
free(buf);
diff --git a/src/gmnlm.c b/src/gmnlm.c
index 0ea492bb85fe024bd250c304808dcb8f0a915f90..aeb0c834d49c799fcdae54acfcbd3925dcedd392 100644
--- a/src/gmnlm.c
+++ b/src/gmnlm.c
@@ -1,5 +1,5 @@
#include <assert.h>
-#include <bearssl_ssl.h>
+#include <bearssl.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
diff --git a/src/tofu.c b/src/tofu.c
index 570bd41c885cd9bb007b58bffca957ace6916bea..0acdf33a50bb4957741f4789472ffe56035a7159 100644
--- a/src/tofu.c
+++ b/src/tofu.c
@@ -1,6 +1,5 @@
#include <assert.h>
-#include <bearssl_hash.h>
-#include <bearssl_x509.h>
+#include <bearssl.h>
#include <errno.h>
#include <gmni/gmni.h>
#include <gmni/tofu.h>
diff --git a/src/util.c b/src/util.c
index 780d0e8803ab9179683bd9a92cbead05624f2681..1cb0bf42b6319e6bd1b0cf0cc94e28627e4f9c68 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1,5 +1,5 @@
#include <assert.h>
-#include <bearssl_ssl.h>
+#include <bearssl.h>
#include <errno.h>
#include <gmni/gmni.h>
#include <libgen.h>