mirror of
https://git.sr.ht/~sircmpwn/gmni
synced 2024-11-26 10:53:46 +01:00
Initial support for client side certificates
This is only supported with gmni for now - gmnlm support will come later. A limitation with BearSSL prevents us from doing automated certificate generation for now, unfortunately.
This commit is contained in:
parent
2017d26c41
commit
1ed4f09532
1
configure
vendored
1
configure
vendored
@ -4,6 +4,7 @@ eval ". $srcdir/config.sh"
|
||||
|
||||
gmni() {
|
||||
genrules gmni \
|
||||
src/certs.c \
|
||||
src/client.c \
|
||||
src/escape.c \
|
||||
src/gmni.c \
|
||||
|
@ -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
|
||||
|
27
include/gmni/certs.h
Normal file
27
include/gmni/certs.h
Normal file
@ -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
|
@ -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 @@ 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;
|
||||
|
@ -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 {
|
||||
|
156
src/certs.c
Normal file
156
src/certs.c
Normal file
@ -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;
|
||||
}
|
22
src/client.c
22
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 @@ 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);
|
||||
|
||||
|
48
src/gmni.c
48
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/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 @@ 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]);
|
||||
@ -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);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_ssl.h>
|
||||
#include <bearssl.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
|
@ -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>
|
||||
|
@ -1,5 +1,5 @@
|
||||
#include <assert.h>
|
||||
#include <bearssl_ssl.h>
|
||||
#include <bearssl.h>
|
||||
#include <errno.h>
|
||||
#include <gmni/gmni.h>
|
||||
#include <libgen.h>
|
||||
|
Loading…
Reference in New Issue
Block a user