1 /* $Id: revokeproc.c,v 1.25 2022/12/18 12:04:55 tb Exp $ */ 2 /* 3 * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> 4 * 5 * Permission to use, copy, modify, and distribute this software for any 6 * purpose with or without fee is hereby granted, provided that the above 7 * copyright notice and this permission notice appear in all copies. 8 * 9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES 10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR 12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 */ 17 18 #include <assert.h> 19 #include <ctype.h> 20 #include <err.h> 21 #include <errno.h> 22 #include <stdio.h> 23 #include <stdlib.h> 24 #include <string.h> 25 #include <unistd.h> 26 #include <vis.h> 27 28 #include <openssl/pem.h> 29 #include <openssl/x509.h> 30 #include <openssl/x509v3.h> 31 #include <openssl/err.h> 32 33 #include "extern.h" 34 35 #define RENEW_ALLOW (30 * 24 * 60 * 60) 36 37 /* 38 * Convert the X509's expiration time into a time_t value. 39 */ 40 static time_t 41 X509expires(X509 *x) 42 { 43 ASN1_TIME *atim; 44 struct tm t; 45 46 if ((atim = X509_getm_notAfter(x)) == NULL) { 47 warnx("missing notAfter"); 48 return -1; 49 } 50 51 memset(&t, 0, sizeof(t)); 52 53 if (!ASN1_TIME_to_tm(atim, &t)) { 54 warnx("invalid ASN1_TIME"); 55 return -1; 56 } 57 58 return timegm(&t); 59 } 60 61 int 62 revokeproc(int fd, const char *certfile, int force, 63 int revocate, const char *const *alts, size_t altsz) 64 { 65 GENERAL_NAMES *sans = NULL; 66 char *der = NULL, *dercp, *der64 = NULL; 67 int rc = 0, cc, i, len; 68 size_t *found = NULL; 69 FILE *f = NULL; 70 X509 *x = NULL; 71 long lval; 72 enum revokeop op, rop; 73 time_t t; 74 size_t j; 75 76 /* 77 * First try to open the certificate before we drop privileges 78 * and jail ourselves. 79 * We allow "f" to be NULL IFF the cert doesn't exist yet. 80 */ 81 82 if ((f = fopen(certfile, "r")) == NULL && errno != ENOENT) { 83 warn("%s", certfile); 84 goto out; 85 } 86 87 /* File-system and sandbox jailing. */ 88 89 ERR_load_crypto_strings(); 90 91 if (pledge("stdio", NULL) == -1) { 92 warn("pledge"); 93 goto out; 94 } 95 96 /* 97 * If we couldn't open the certificate, it doesn't exist so we 98 * haven't submitted it yet, so obviously we can mark that it 99 * has expired and we should renew it. 100 * If we're revoking, however, then that's an error! 101 * Ignore if the reader isn't reading in either case. 102 */ 103 104 if (f == NULL && revocate) { 105 warnx("%s: no certificate found", certfile); 106 (void)writeop(fd, COMM_REVOKE_RESP, REVOKE_OK); 107 goto out; 108 } else if (f == NULL && !revocate) { 109 if (writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP) >= 0) 110 rc = 1; 111 goto out; 112 } 113 114 if ((x = PEM_read_X509(f, NULL, NULL, NULL)) == NULL) { 115 warnx("PEM_read_X509"); 116 goto out; 117 } 118 119 /* Cache and sanity check X509v3 extensions. */ 120 121 if (X509_check_purpose(x, -1, -1) <= 0) { 122 warnx("%s: invalid X509v3 extensions", certfile); 123 goto out; 124 } 125 126 /* Read out the expiration date. */ 127 128 if ((t = X509expires(x)) == -1) { 129 warnx("X509expires"); 130 goto out; 131 } 132 133 /* Extract list of SAN entries from the certificate. */ 134 135 sans = X509_get_ext_d2i(x, NID_subject_alt_name, NULL, NULL); 136 if (sans == NULL) { 137 warnx("%s: does not have a SAN entry", certfile); 138 if (revocate) 139 goto out; 140 force = 2; 141 } 142 143 /* An array of buckets: the number of entries found. */ 144 145 if ((found = calloc(altsz, sizeof(size_t))) == NULL) { 146 warn("calloc"); 147 goto out; 148 } 149 150 /* 151 * Ensure the certificate's SAN entries fully cover those from the 152 * configuration file and that all domains are represented only once. 153 */ 154 155 for (i = 0; i < sk_GENERAL_NAME_num(sans); i++) { 156 GENERAL_NAME *gen_name; 157 const ASN1_IA5STRING *name; 158 const unsigned char *name_buf; 159 int name_len; 160 int name_type; 161 162 gen_name = sk_GENERAL_NAME_value(sans, i); 163 assert(gen_name != NULL); 164 165 name = GENERAL_NAME_get0_value(gen_name, &name_type); 166 if (name_type != GEN_DNS) 167 continue; 168 169 /* name_buf isn't a C string and could contain embedded NULs. */ 170 name_buf = ASN1_STRING_get0_data(name); 171 name_len = ASN1_STRING_length(name); 172 173 for (j = 0; j < altsz; j++) { 174 if ((size_t)name_len != strlen(alts[j])) 175 continue; 176 if (memcmp(name_buf, alts[j], name_len) == 0) 177 break; 178 } 179 if (j == altsz) { 180 if (revocate) { 181 char *visbuf; 182 183 visbuf = calloc(4, name_len + 1); 184 if (visbuf == NULL) { 185 warn("%s: unexpected SAN", certfile); 186 goto out; 187 } 188 strvisx(visbuf, name_buf, name_len, VIS_SAFE); 189 warnx("%s: unexpected SAN entry: %s", 190 certfile, visbuf); 191 free(visbuf); 192 goto out; 193 } 194 force = 2; 195 continue; 196 } 197 if (found[j]++) { 198 if (revocate) { 199 warnx("%s: duplicate SAN entry: %.*s", 200 certfile, name_len, name_buf); 201 goto out; 202 } 203 force = 2; 204 } 205 } 206 207 for (j = 0; j < altsz; j++) { 208 if (found[j]) 209 continue; 210 if (revocate) { 211 warnx("%s: domain not listed: %s", certfile, alts[j]); 212 goto out; 213 } 214 force = 2; 215 } 216 217 /* 218 * If we're going to revoke, write the certificate to the 219 * netproc in DER and base64-encoded format. 220 * Then exit: we have nothing left to do. 221 */ 222 223 if (revocate) { 224 dodbg("%s: revocation", certfile); 225 226 /* 227 * First, tell netproc we're online. 228 * If they're down, then just exit without warning. 229 */ 230 231 cc = writeop(fd, COMM_REVOKE_RESP, REVOKE_EXP); 232 if (cc == 0) 233 rc = 1; 234 if (cc <= 0) 235 goto out; 236 237 if ((len = i2d_X509(x, NULL)) < 0) { 238 warnx("i2d_X509"); 239 goto out; 240 } else if ((der = dercp = malloc(len)) == NULL) { 241 warn("malloc"); 242 goto out; 243 } else if (len != i2d_X509(x, (u_char **)&dercp)) { 244 warnx("i2d_X509"); 245 goto out; 246 } else if ((der64 = base64buf_url(der, len)) == NULL) { 247 warnx("base64buf_url"); 248 goto out; 249 } else if (writestr(fd, COMM_CSR, der64) >= 0) 250 rc = 1; 251 252 goto out; 253 } 254 255 rop = time(NULL) >= (t - RENEW_ALLOW) ? REVOKE_EXP : REVOKE_OK; 256 257 if (rop == REVOKE_EXP) 258 dodbg("%s: certificate renewable: %lld days left", 259 certfile, (long long)(t - time(NULL)) / 24 / 60 / 60); 260 else 261 dodbg("%s: certificate valid: %lld days left", 262 certfile, (long long)(t - time(NULL)) / 24 / 60 / 60); 263 264 if (rop == REVOKE_OK && force) { 265 warnx("%s: %sforcing renewal", certfile, 266 force == 2 ? "domain list changed, " : ""); 267 rop = REVOKE_EXP; 268 } 269 270 /* 271 * We can re-submit it given RENEW_ALLOW time before. 272 * If netproc is down, just exit. 273 */ 274 275 if ((cc = writeop(fd, COMM_REVOKE_RESP, rop)) == 0) 276 rc = 1; 277 if (cc <= 0) 278 goto out; 279 280 op = REVOKE__MAX; 281 if ((lval = readop(fd, COMM_REVOKE_OP)) == 0) 282 op = REVOKE_STOP; 283 else if (lval == REVOKE_CHECK) 284 op = lval; 285 286 if (op == REVOKE__MAX) { 287 warnx("unknown operation from netproc"); 288 goto out; 289 } else if (op == REVOKE_STOP) { 290 rc = 1; 291 goto out; 292 } 293 294 rc = 1; 295 out: 296 close(fd); 297 if (f != NULL) 298 fclose(f); 299 X509_free(x); 300 GENERAL_NAMES_free(sans); 301 free(der); 302 free(found); 303 free(der64); 304 ERR_print_errors_fp(stderr); 305 ERR_free_strings(); 306 return rc; 307 } 308