diff --git a/configure b/configure index e82a0e2..8d3b67a 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 1f20672..866dd41 100644 --- a/doc/gmni.scd +++ b/doc/gmni.scd @@ -38,11 +38,9 @@ performed with the user's input supplied to the server. 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 0000000..22e226d --- /dev/null +++ b/include/gmni/certs.h @@ -0,0 +1,27 @@ +#ifndef GEMINI_CERTS_H +#define GEMINI_CERTS_H +#include +#include + +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 16bef51..22295e2 100644 --- a/include/gmni/gmni.h +++ b/include/gmni/gmni.h @@ -1,6 +1,6 @@ #ifndef GEMINI_CLIENT_H #define GEMINI_CLIENT_H -#include +#include #include #include #include @@ -61,6 +61,8 @@ struct gemini_response { 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 @@ struct gemini_options { // 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 a0981a5..51d1d60 100644 --- a/include/gmni/tofu.h +++ b/include/gmni/tofu.h @@ -1,6 +1,6 @@ #ifndef GEMINI_TOFU_H #define GEMINI_TOFU_H -#include +#include #include enum tofu_error { diff --git a/src/certs.c b/src/certs.c new file mode 100644 index 0000000..f40bfa7 --- /dev/null +++ b/src/certs.c @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include +#include +#include + +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 e402cc9..127a56c 100644 --- a/src/client.c +++ b/src/client.c @@ -1,13 +1,14 @@ #include #include #include -#include +#include #include #include #include #include #include #include +#include #include #include #include @@ -169,7 +170,26 @@ gemini_request(const char *url, struct gemini_options *options, // 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 a8321d0..f3015ac 100644 --- a/src/gmni.c +++ b/src/gmni.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -109,6 +110,45 @@ tofu_callback(enum tofu_error error, const char *fingerprint, 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 @@ main(int argc, char *argv[]) } 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 @@ main(int argc, char *argv[]) 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 @@ main(int argc, char *argv[]) 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 c8c301c..768009d 100644 --- a/src/gmnlm.c +++ b/src/gmnlm.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include diff --git a/src/tofu.c b/src/tofu.c index 570bd41..0acdf33 100644 --- a/src/tofu.c +++ b/src/tofu.c @@ -1,6 +1,5 @@ #include -#include -#include +#include #include #include #include diff --git a/src/util.c b/src/util.c index 780d0e8..1cb0bf4 100644 --- a/src/util.c +++ b/src/util.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include #include