[BACK]Return to acctproc.c CVS log [TXT][DIR] Up to [local] / src / usr.sbin / acme-client

File: [local] / src / usr.sbin / acme-client / acctproc.c (download)

Revision 1.32, Tue Aug 29 14:44:53 2023 UTC (9 months ago) by op
Branch: MAIN
CVS Tags: OPENBSD_7_5_BASE, OPENBSD_7_5, OPENBSD_7_4_BASE, OPENBSD_7_4, HEAD
Changes since 1.31: +1 -2 lines

acme-client: drop ecdsa.h, fix spacing and a typo in error message

While here drop EC_KEY_set_asn1_flag(OPENSSL_EC_NAMED_CURVE).
EC_KEY_new_by_curve_name() ends up calling EC_GROUP_new() which already
sets the OPENSSL_EC_NAMED_CURVE flag on the group.  (suggested by tb@)

ok tb@

/*	$Id: acctproc.c,v 1.32 2023/08/29 14:44:53 op Exp $ */
/*
 * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include <sys/stat.h>

#include <err.h>
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/err.h>

#include "extern.h"
#include "key.h"

/*
 * Converts a BIGNUM to the form used in JWK.
 * This is essentially a base64-encoded big-endian binary string
 * representation of the number.
 */
static char *
bn2string(const BIGNUM *bn)
{
	int	 len;
	char	*buf, *bbuf;

	/* Extract big-endian representation of BIGNUM. */

	len = BN_num_bytes(bn);
	if ((buf = malloc(len)) == NULL) {
		warn("malloc");
		return NULL;
	} else if (len != BN_bn2bin(bn, (unsigned char *)buf)) {
		warnx("BN_bn2bin");
		free(buf);
		return NULL;
	}

	/* Convert to base64url. */

	if ((bbuf = base64buf_url(buf, len)) == NULL) {
		warnx("base64buf_url");
		free(buf);
		return NULL;
	}

	free(buf);
	return bbuf;
}

/*
 * Extract the relevant RSA components from the key and create the JSON
 * thumbprint from them.
 */
static char *
op_thumb_rsa(EVP_PKEY *pkey)
{
	char	*exp = NULL, *mod = NULL, *json = NULL;
	RSA	*r;

	if ((r = EVP_PKEY_get0_RSA(pkey)) == NULL)
		warnx("EVP_PKEY_get0_RSA");
	else if ((mod = bn2string(RSA_get0_n(r))) == NULL)
		warnx("bn2string");
	else if ((exp = bn2string(RSA_get0_e(r))) == NULL)
		warnx("bn2string");
	else if ((json = json_fmt_thumb_rsa(exp, mod)) == NULL)
		warnx("json_fmt_thumb_rsa");

	free(exp);
	free(mod);
	return json;
}

/*
 * Extract the relevant EC components from the key and create the JSON
 * thumbprint from them.
 */
static char *
op_thumb_ec(EVP_PKEY *pkey)
{
	BIGNUM	*X = NULL, *Y = NULL;
	EC_KEY	*ec = NULL;
	char	*x = NULL, *y = NULL;
	char	*json = NULL;

	if ((ec = EVP_PKEY_get0_EC_KEY(pkey)) == NULL)
		warnx("EVP_PKEY_get0_EC_KEY");
	else if ((X = BN_new()) == NULL)
		warnx("BN_new");
	else if ((Y = BN_new()) == NULL)
		warnx("BN_new");
	else if (!EC_POINT_get_affine_coordinates(EC_KEY_get0_group(ec),
	    EC_KEY_get0_public_key(ec), X, Y, NULL))
		warnx("EC_POINT_get_affine_coordinates");
	else if ((x = bn2string(X)) == NULL)
		warnx("bn2string");
	else if ((y = bn2string(Y)) == NULL)
		warnx("bn2string");
	else if ((json = json_fmt_thumb_ec(x, y)) == NULL)
		warnx("json_fmt_thumb_ec");

	BN_free(X);
	BN_free(Y);
	free(x);
	free(y);
	return json;
}

/*
 * The thumbprint operation is used for the challenge sequence.
 */
static int
op_thumbprint(int fd, EVP_PKEY *pkey)
{
	char		*thumb = NULL, *dig64 = NULL;
	unsigned char	 dig[EVP_MAX_MD_SIZE];
	unsigned int	 digsz;
	int		 rc = 0;

	/* Construct the thumbprint input itself. */

	switch (EVP_PKEY_base_id(pkey)) {
	case EVP_PKEY_RSA:
		if ((thumb = op_thumb_rsa(pkey)) != NULL)
			break;
		goto out;
	case EVP_PKEY_EC:
		if ((thumb = op_thumb_ec(pkey)) != NULL)
			break;
		goto out;
	default:
		warnx("EVP_PKEY_base_id: unknown key type");
		goto out;
	}

	/*
	 * Compute the SHA256 digest of the thumbprint then
	 * base64-encode the digest itself.
	 * If the reader is closed when we write, ignore it (we'll pick
	 * it up in the read loop).
	 */

	if (!EVP_Digest(thumb, strlen(thumb), dig, &digsz, EVP_sha256(),
	    NULL)) {
		warnx("EVP_Digest");
		goto out;
	}
	if ((dig64 = base64buf_url((char *)dig, digsz)) == NULL) {
		warnx("base64buf_url");
		goto out;
	}
	if (writestr(fd, COMM_THUMB, dig64) < 0)
		goto out;

	rc = 1;
out:
	free(thumb);
	free(dig64);
	return rc;
}

static int
op_sign_rsa(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url)
{
	char	*exp = NULL, *mod = NULL;
	int	rc = 0;
	RSA	*r;

	*prot = NULL;

	/*
	 * First, extract relevant portions of our private key.
	 * Finally, format the header combined with the nonce.
	 */

	if ((r = EVP_PKEY_get0_RSA(pkey)) == NULL)
		warnx("EVP_PKEY_get0_RSA");
	else if ((mod = bn2string(RSA_get0_n(r))) == NULL)
		warnx("bn2string");
	else if ((exp = bn2string(RSA_get0_e(r))) == NULL)
		warnx("bn2string");
	else if ((*prot = json_fmt_protected_rsa(exp, mod, nonce, url)) == NULL)
		warnx("json_fmt_protected_rsa");
	else
		rc = 1;

	free(exp);
	free(mod);
	return rc;
}

static int
op_sign_ec(char **prot, EVP_PKEY *pkey, const char *nonce, const char *url)
{
	BIGNUM	*X = NULL, *Y = NULL;
	EC_KEY	*ec = NULL;
	char	*x = NULL, *y = NULL;
	int	rc = 0;

	*prot = NULL;

	if ((ec = EVP_PKEY_get0_EC_KEY(pkey)) == NULL)
		warnx("EVP_PKEY_get0_EC_KEY");
	else if ((X = BN_new()) == NULL)
		warnx("BN_new");
	else if ((Y = BN_new()) == NULL)
		warnx("BN_new");
	else if (!EC_POINT_get_affine_coordinates(EC_KEY_get0_group(ec),
	    EC_KEY_get0_public_key(ec), X, Y, NULL))
		warnx("EC_POINT_get_affine_coordinates");
	else if ((x = bn2string(X)) == NULL)
		warnx("bn2string");
	else if ((y = bn2string(Y)) == NULL)
		warnx("bn2string");
	else if ((*prot = json_fmt_protected_ec(x, y, nonce, url)) == NULL)
		warnx("json_fmt_protected_ec");
	else
		rc = 1;

	BN_free(X);
	BN_free(Y);
	free(x);
	free(y);
	return rc;
}

/*
 * Operation to sign a message with the account key.
 * This requires the sender ("fd") to provide the payload and a nonce.
 */
static int
op_sign(int fd, EVP_PKEY *pkey, enum acctop op)
{
	EVP_MD_CTX		*ctx = NULL;
	const EVP_MD		*evp_md = NULL;
	ECDSA_SIG		*ec_sig = NULL;
	const BIGNUM		*ec_sig_r = NULL, *ec_sig_s = NULL;
	int			 bn_len, sign_len, rc = 0;
	char			*nonce = NULL, *pay = NULL, *pay64 = NULL;
	char			*prot = NULL, *prot64 = NULL;
	char			*sign = NULL, *dig64 = NULL, *fin = NULL;
	char			*url = NULL, *kid = NULL, *alg = NULL;
	const unsigned char	*digp;
	unsigned char		*dig = NULL, *buf = NULL;
	size_t			 digsz;

	/* Read our payload and nonce from the requestor. */

	if ((pay = readstr(fd, COMM_PAY)) == NULL)
		goto out;
	else if ((nonce = readstr(fd, COMM_NONCE)) == NULL)
		goto out;
	else if ((url = readstr(fd, COMM_URL)) == NULL)
		goto out;

	if (op == ACCT_KID_SIGN)
		if ((kid = readstr(fd, COMM_KID)) == NULL)
			goto out;

	/* Base64-encode the payload. */

	if ((pay64 = base64buf_url(pay, strlen(pay))) == NULL) {
		warnx("base64buf_url");
		goto out;
	}

	switch (EVP_PKEY_base_id(pkey)) {
	case EVP_PKEY_RSA:
		alg = "RS256";
		evp_md = EVP_sha256();
		break;
	case EVP_PKEY_EC:
		alg = "ES384";
		evp_md = EVP_sha384();
		break;
	default:
		warnx("unknown account key type");
		goto out;
	}

	if (op == ACCT_KID_SIGN) {
		if ((prot = json_fmt_protected_kid(alg, kid, nonce, url)) ==
		    NULL) {
			warnx("json_fmt_protected_kid");
			goto out;
		}
	} else {
		switch (EVP_PKEY_base_id(pkey)) {
		case EVP_PKEY_RSA:
			if (!op_sign_rsa(&prot, pkey, nonce, url))
				goto out;
			break;
		case EVP_PKEY_EC:
			if (!op_sign_ec(&prot, pkey, nonce, url))
				goto out;
			break;
		default:
			warnx("EVP_PKEY_base_id");
			goto out;
		}
	}

	/* The header combined with the nonce, base64. */

	if ((prot64 = base64buf_url(prot, strlen(prot))) == NULL) {
		warnx("base64buf_url");
		goto out;
	}

	/* Now the signature material. */

	sign_len = asprintf(&sign, "%s.%s", prot64, pay64);
	if (sign_len == -1) {
		warn("asprintf");
		sign = NULL;
		goto out;
	}

	/* Sign the message. */

	if ((ctx = EVP_MD_CTX_new()) == NULL) {
		warnx("EVP_MD_CTX_new");
		goto out;
	}
	if (!EVP_DigestSignInit(ctx, NULL, evp_md, NULL, pkey)) {
		warnx("EVP_DigestSignInit");
		goto out;
	}
	if (!EVP_DigestSign(ctx, NULL, &digsz, sign, sign_len)) {
		warnx("EVP_DigestSign");
		goto out;
	}
	if ((dig = malloc(digsz)) == NULL) {
		warn("malloc");
		goto out;
	}
	if (!EVP_DigestSign(ctx, dig, &digsz, sign, sign_len)) {
		warnx("EVP_DigestSign");
		goto out;
	}

	switch (EVP_PKEY_base_id(pkey)) {
	case EVP_PKEY_RSA:
		if ((dig64 = base64buf_url((char *)dig, digsz)) == NULL) {
			warnx("base64buf_url");
			goto out;
		}
		break;
	case EVP_PKEY_EC:
		if (digsz > LONG_MAX) {
			warnx("EC signature too long");
			goto out;
		}

		digp = dig;
		if ((ec_sig = d2i_ECDSA_SIG(NULL, &digp, digsz)) == NULL) {
			warnx("d2i_ECDSA_SIG");
			goto out;
		}

		if ((ec_sig_r = ECDSA_SIG_get0_r(ec_sig)) == NULL ||
		    (ec_sig_s = ECDSA_SIG_get0_s(ec_sig)) == NULL) {
			warnx("ECDSA_SIG_get0");
			goto out;
		}

		if ((bn_len = (EVP_PKEY_bits(pkey) + 7) / 8) <= 0) {
			warnx("EVP_PKEY_bits");
			goto out;
		}

		if ((buf = calloc(2, bn_len)) == NULL) {
			warnx("calloc");
			goto out;
		}

		if (BN_bn2binpad(ec_sig_r, buf, bn_len) != bn_len ||
		    BN_bn2binpad(ec_sig_s, buf + bn_len, bn_len) != bn_len) {
			warnx("BN_bn2binpad");
			goto out;
		}

		if ((dig64 = base64buf_url((char *)buf, 2 * bn_len)) == NULL) {
			warnx("base64buf_url");
			goto out;
		}

		break;
	default:
		warnx("EVP_PKEY_base_id");
		goto out;
	}

	/*
	 * Write back in the correct JSON format.
	 * If the reader is closed, just ignore it (we'll pick it up
	 * when we next enter the read loop).
	 */

	if ((fin = json_fmt_signed(prot64, pay64, dig64)) == NULL) {
		warnx("json_fmt_signed");
		goto out;
	} else if (writestr(fd, COMM_REQ, fin) < 0)
		goto out;

	rc = 1;
out:
	ECDSA_SIG_free(ec_sig);
	EVP_MD_CTX_free(ctx);
	free(pay);
	free(sign);
	free(pay64);
	free(url);
	free(nonce);
	free(kid);
	free(prot);
	free(prot64);
	free(dig);
	free(dig64);
	free(fin);
	free(buf);
	return rc;
}

int
acctproc(int netsock, const char *acctkey, enum keytype keytype)
{
	FILE		*f = NULL;
	EVP_PKEY	*pkey = NULL;
	long		 lval;
	enum acctop	 op;
	int		 rc = 0, cc, newacct = 0;
	mode_t		 prev;

	/*
	 * First, open our private key file read-only or write-only if
	 * we're creating from scratch.
	 * Set our umask to be maximally restrictive.
	 */

	prev = umask((S_IWUSR | S_IXUSR) | S_IRWXG | S_IRWXO);
	if ((f = fopen(acctkey, "r")) == NULL && errno == ENOENT) {
		f = fopen(acctkey, "wx");
		newacct = 1;
	}
	umask(prev);

	if (f == NULL) {
		warn("%s", acctkey);
		goto out;
	}

	/* File-system, user, and sandbox jailing. */

	ERR_load_crypto_strings();

	if (pledge("stdio", NULL) == -1) {
		warn("pledge");
		goto out;
	}

	if (newacct) {
		switch (keytype) {
		case KT_ECDSA:
			if ((pkey = ec_key_create(f, acctkey)) == NULL)
				goto out;
			dodbg("%s: generated ECDSA account key", acctkey);
			break;
		case KT_RSA:
			if ((pkey = rsa_key_create(f, acctkey)) == NULL)
				goto out;
			dodbg("%s: generated RSA account key", acctkey);
			break;
		}
	} else {
		if ((pkey = key_load(f, acctkey)) == NULL)
			goto out;
		/* XXX check if account key type equals configured key type */
		doddbg("%s: loaded account key", acctkey);
	}

	fclose(f);
	f = NULL;

	/* Notify the netproc that we've started up. */

	if ((cc = writeop(netsock, COMM_ACCT_STAT, ACCT_READY)) == 0)
		rc = 1;
	if (cc <= 0)
		goto out;

	/*
	 * Now we wait for requests from the network-facing process.
	 * It might ask us for our thumbprint, for example, or for us to
	 * sign a message.
	 */

	for (;;) {
		op = ACCT__MAX;
		if ((lval = readop(netsock, COMM_ACCT)) == 0)
			op = ACCT_STOP;
		else if (lval == ACCT_SIGN || lval == ACCT_KID_SIGN ||
		    lval == ACCT_THUMBPRINT)
			op = lval;

		if (ACCT__MAX == op) {
			warnx("unknown operation from netproc");
			goto out;
		} else if (ACCT_STOP == op)
			break;

		switch (op) {
		case ACCT_SIGN:
		case ACCT_KID_SIGN:
			if (op_sign(netsock, pkey, op))
				break;
			warnx("op_sign");
			goto out;
		case ACCT_THUMBPRINT:
			if (op_thumbprint(netsock, pkey))
				break;
			warnx("op_thumbprint");
			goto out;
		default:
			abort();
		}
	}

	rc = 1;
out:
	close(netsock);
	if (f != NULL)
		fclose(f);
	EVP_PKEY_free(pkey);
	ERR_print_errors_fp(stderr);
	ERR_free_strings();
	return rc;
}