Branch :
/* $Id: revokeproc.c,v 1.25 2022/12/18 12:04:55 tb 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 <assert.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <vis.h>
#include <openssl/pem.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <openssl/err.h>
#include "extern.h"
#define RENEW_ALLOW (30 * 24 * 60 * 60)
/*
* Convert the X509's expiration time into a time_t value.
*/
static time_t
X509expires(X509 *x)
{
ASN1_TIME *atim;
struct tm t;
if ((atim = X509_getm_notAfter(x)) == NULL) {
warnx("missing notAfter");
return -1;
}
memset(&t, 0, sizeof(t));
if (!ASN1_TIME_to_tm(atim, &t)) {
warnx("invalid ASN1_TIME");
return -1;
}
return timegm(&t);
}
int
revokeproc(int fd, const char *certfile, int force,
int revocate, const char *const *alts, size_t altsz)
{
GENERAL_NAMES *sans = NULL;
char *der = NULL, *dercp, *der64 = NULL;
int rc = 0, cc, i, len;
size_t *found = NULL;
FILE *f = NULL;
X509 *x = NULL;
long lval;
enum revokeop op, rop;
time_t t;
size_t j;
/*
* First try to open the certificate before we drop privileges
* and jail ourselves.
* We allow "f" to be NULL IFF the cert doesn't exist yet.
*/
if ((f = fopen(certfile, "r")) == NULL && errno != ENOENT) {
warn("%s", certfile);
goto out;
}
/* File-system and sandbox jailing. */
ERR_load_crypto_strings();
#if defined(__OpenBSD__)
if (pledge("stdio", NULL) == -1) {
warn("pledge");
goto out;
}
#endif
/*
* If we couldn't open the certificate, it doesn't exist so we
* haven't submitted it yet, so obviously we can mark that it
* has expired and we should renew it.
* If we're revoking, however, then that's an error!
* Ignore if the reader isn't reading in either case.
*/
if (f == NULL && revocate) {
warnx("%s: no certificate found", certfile);
(void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK);
goto out;
} else if (f == NULL && !revocate) {
if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0)
rc = 1;
goto out;
}
if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) {
warnx("PEM_read_X509");
goto out;
}
/* Cache and sanity check X509v3 extensions. */
if (X509_check_purpose(x, -1, -1) <= 0) {
warnx("%s: invalid X509v3 extensions", certfile);
goto out;
}
/* Read out the expiration date. */
if ((t = X509expires(x)) == -1) {
warnx("X509expires");
goto out;
}
/* Extract list of SAN entries from the certificate. */
sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL);
if (sans == NULL) {
warnx("%s: does not have a SAN entry", certfile);
if (revocate)
goto out;
force = 2;
}
/* An array of buckets: the number of entries found. */
if ((found = calloc(altsz, sizeof(size_t))) == NULL) {
warn("calloc");
goto out;
}
/*
* Ensure the certificate's SAN entries fully cover those from the
* configuration file and that all domains are represented only once.
*/
for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) {
GENERAL_NAME *gen_name;
const ASN1_IA5STRING *name;
const unsigned char *name_buf;
int name_len;
int name_type;
gen_name = sk_GENERAL_NAME_value(sans, i);
assert(gen_name != NULL);
name = GENERAL_NAME_get0_value(gen_name, &name_type);
if (name_type != GEN_DNS)
continue;
/* name_buf isn't a C string and could contain embedded NULs. */
name_buf = ASN1_STRING_get0_data(name);
name_len = ASN1_STRING_length(name);
for (j = 0; j < altsz; j++) {
if ((size_t)name_len != strlen(alts[j]))
continue;
if (memcmp(name_buf, alts[j], name_len) == 0)
break;
}
if (j == altsz) {
if (revocate) {
char *visbuf;
visbuf = calloc(4, name_len + 1);
if (visbuf == NULL) {
warn("%s: unexpected SAN", certfile);
goto out;
}
strvisx(visbuf, (const char *)name_buf,
name_len, VIS_SAFE);
warnx("%s: unexpected SAN entry: %s",
certfile, visbuf);
free(visbuf);
goto out;
}
force = 2;
continue;
}
if (found[j]++) {
if (revocate) {
warnx("%s: duplicate SAN entry: %.*s",
certfile, name_len, name_buf);
goto out;
}
force = 2;
}
}
for (j = 0; j < altsz; j++) {
if (found[j])
continue;
if (revocate) {
warnx("%s: domain not listed: %s", certfile, alts[j]);
goto out;
}
force = 2;
}
/*
* If we're going to revoke, write the certificate to the
* netproc in DER and base64-encoded format.
* Then exit: we have nothing left to do.
*/
if (revocate) {
dodbg("%s: revocation", certfile);
/*
* First, tell netproc we're online.
* If they're down, then just exit without warning.
*/
cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP);
if (cc == 0)
rc = 1;
if (cc <= 0)
goto out;
if ((len = i2d_X509(x, NULL)) < 0) {
warnx("i2d_X509");
goto out;
} else if ((der = dercp = malloc(len)) == NULL) {
warn("malloc");
goto out;
} else if (len != i2d_X509(x, (u_char **)&dercp)) {
warnx("i2d_X509");
goto out;
} else if ((der64 = base64buf_url(der, len)) == NULL) {
warnx("base64buf_url");
goto out;
} else if (writestr(fd, COMM_CSR, der64) >= 0)
rc = 1;
goto out;
}
rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK;
if (rop == REVOKE_EXP)
dodbg("%s: certificate renewable: %lld days left",
certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
else
dodbg("%s: certificate valid: %lld days left",
certfile, (long long)(t - time(NULL)) / 24 / 60 / 60);
if (rop == REVOKE_OK && force) {
warnx("%s: %sforcing renewal", certfile,
force == 2 ? "domain list changed, " : "");
rop = REVOKE_EXP;
}
/*
* We can re-submit it given RENEW_ALLOW time before.
* If netproc is down, just exit.
*/
if ((cc = writeop(fd, COMM_REVOKE_RESP, rop)) == 0)
rc = 1;
if (cc <= 0)
goto out;
op = REVOKE__MAX;
if ((lval = readop(fd, COMM_REVOKE_OP)) == 0)
op = REVOKE_STOP;
else if (lval == REVOKE_CHECK)
op = lval;
if (op == REVOKE__MAX) {
warnx("unknown operation from netproc");
goto out;
} else if (op == REVOKE_STOP) {
rc = 1;
goto out;
}
rc = 1;
out:
close(fd);
if (f != NULL)
fclose(f);
X509_free(x);
GENERAL_NAMES_free(sans);
free(der);
free(found);
free(der64);
ERR_print_errors_fp(stderr);
ERR_free_strings();
return rc;
}