/*****************************************************************************/ #ifdef COMMENTS_WITH_COMMENTS /* (wu)certman.c Certificate management functions. COPYRIGHT --------- Copyright (C) 2020,2024 Mark G.Daniel This program comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it under the conditions of the GNU GENERAL PUBLIC LICENSE, version 3, or any later version. http://www.gnu.org/licenses/gpl.txt VERSION HISTORY --------------- 21-APR-2021 MGD modify CertManLock() for non-shared cluster members 03-JUN-2020 MGD enhance CertManLoad() for WUCME_LOAD 14-DEC-2019 MGD adapted from wCME 23-APR-2017 MGD initial development */ #endif /* COMMENTS_WITH_COMMENTS */ /*****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "wucme.h" #include "wucertman.h" #include "wureport.h" #define FI_LI "CERTMAN", __LINE__ static char Utility [] = "WUCME"; static char *CgiPlusEsc, *CgiPlusEot; extern int wucmeActive; extern uchar SyiClusterMember; extern char SoftwareId[], SyiNodeName[]; extern char *argv0; char* doasprintf (char*, ...); /*****************************************************************************/ /* Search the certificate directory for certificate files and pass the names of files found to the expiy date check function. If within the expiry period have Let's Encrypt re-certify the hosts represented by the certificate. */ void CertManBegin (void) { int certcnt, failcnt, filecnt, length, renew, retval, spawned01, succnt; char *cptr, *certdir, *fname, *services; DIR *dirptr; struct dirent *dentptr; /*********/ /* begin */ /*********/ certdir = strdup(wucmeDir()); OpenSSL_add_all_algorithms(); dirptr = opendir (certdir); if (!dirptr) { warnx ("opendir() %s:%d %%X%08.08X", FI_LI, vaxc$errno); EXIT_FI_LI (vaxc$errno); } certcnt = failcnt = filecnt = spawned01 = succnt = 0; for (dentptr = readdir(dirptr); dentptr; dentptr = readdir(dirptr)) { filecnt++; /* only process the actual file in use (ignore the others) */ if (strncasecmp (dentptr->d_name, "wucme_c_", 8)) continue; for (cptr = dentptr->d_name; *cptr; cptr++); while (cptr > dentptr->d_name && *cptr != '.') cptr--; if (strcasecmp (cptr, ".pem")) continue; certcnt++; fname = doasprintf ("%s/%s", certdir, dentptr->d_name); renew = CertManCheck (fname, &services); if (renew < 0) { /* less than CERTMAN_RENEW_DAYS (30) */ if (retval = CertManIssue (services)) failcnt++; else succnt++; if ((length = strlen (services)) > 192) length = 192; cptr = doasprintf ("wuCME %s %*.*s renewal\n%s\n%s", retval ? "FAILED" : "successful", length, length, services, (char*)UtilVmsName(fname,-1), services); ReportThis (cptr); free (cptr); free (services); } free (fname); } closedir (dirptr); free (certdir); if (succnt) CertManLoad (); wucmeChallenge (NULL, NULL); warnx ("%d files, %d certs, %d renewed, %d failed", filecnt, certcnt, succnt, failcnt); } /*****************************************************************************/ /* Parse the certificate content and check the expiry date against today's date the configured pre-expiry period of grace. Return an integer number of days with respect to expiry, with a non-positive number indicating it should be renewed. While processing the certificate create a list of the services supported by the certificate and modify the |services| pointer to point to this list. */ int CertManCheck ( char *cname, char **services ) { static char *altnames; int altcnt, rdays, dday, dsec; char *cptr, *common, *issuer, *subject; char notafter [128], notbefore [128]; ASN1_TIME *asn1aft, *asn1bef; FILE *fp; X509 *cp; /*********/ /* begin */ /*********/ warnx ("%s", (char*)UtilVmsName(cname,-1)); if (altnames) { free (altnames); altnames = NULL; } *services = NULL; /* this is the number of days out from expiry before renewal */ if (cptr = UtilSysTrnLnm (WUCME_DAYS)) { /* but really intended for development purposes only */ rdays = atoi(cptr); if (rdays <= 0 || rdays >= 90) rdays = CERTMAN_RENEW_DAYS; } else rdays = CERTMAN_RENEW_DAYS; fp = fopen (cname, "r"); if (!fp) { warnx ("fopen() %%X%08.08X %s %s", vaxc$errno, strerror(errno), cname); return (-1); } cp = PEM_read_X509(fp, NULL, NULL, NULL); if (!cp) { fclose(fp); warnx ("PEM_read_X509() failed %s", cname); return (-1); } /* in case of failure far enough out */ dday = 999; issuer = X509_NAME_oneline(X509_get_issuer_name(cp), NULL, 0); subject = X509_NAME_oneline(X509_get_subject_name(cp), NULL, 0); asn1bef = X509_get_notBefore (cp); asn1aft = X509_get_notAfter (cp); if (CertManAsn1time (asn1bef, notbefore, sizeof(notbefore))) { warnx ("FromAsn1time(X509_get_notBefore()) failed"); goto out; } if (CertManAsn1time (asn1aft, notafter, sizeof(notafter))) { warnx ("FromAsn1time(X509_get_notAfter()) failed"); goto out; } if (!ASN1_TIME_diff (&dday, &dsec, NULL, asn1aft)) { warnx ("ASN1_TIME_diff() failed"); goto out; } if (!strncasecmp(subject,"/CN=",4)) common = strdup (subject+4); else common = strdup (subject); altnames = CertManAltNames (cp, common); if (strlen(altnames)) free (common); else altnames = common; *services = altnames; warnx ("%s !before: %s !after: %s expires: %d renew: %d", altnames, notbefore, notafter, dday, dday - rdays); out: OPENSSL_free (issuer); OPENSSL_free (subject); X509_free(cp); fclose(fp); return (dday - rdays); } /*****************************************************************************/ /* Issue a certificate for the following services (host names). */ int CertManIssue (char *services) { int argsc, cnt; char *cptr; #define MAX_ARGSV 128 char *argsv [MAX_ARGSV]; /*********/ /* begin */ /*********/ memset (argsv, argsc = 0, sizeof(argsv)); argsv[argsc++] = argv0; argsv[argsc++] = "--uacme"; if (UtilSysTrnLnm (WUCME_VERBOSE)) argsv[argsc++] = "--verbose"; argsv[argsc++] = "issue"; for (cptr = services; *cptr;) { while (*cptr && *cptr == ' ') cptr++; if (!*cptr) break; if (argsc > MAX_ARGSV) break; argsv[argsc++] = cptr; while (*cptr && *cptr != ' ') cptr++; if (*cptr) *cptr++ = '\0'; } wucmeArgcArgv (argsc, argsv); return (mainline (argsc, argsv)); } /*****************************************************************************/ /* Return a pointer to an allocated string containing the certificate's alternative names. */ char* CertManAltNames ( X509 *cp, char *common ) { int altsize, idx, len, namecnt; GENERAL_NAME *entry; GENERAL_NAMES *names; char *cptr, *altnames; uchar *utf8; /*********/ /* begin */ /*********/ utf8 = NULL; altnames = calloc (1, altsize = strlen(common)+2); strcat (altnames, common); if (!cp) return (altnames); names = X509_get_ext_d2i (cp, NID_subject_alt_name, 0, 0); if (!names) return (altnames); namecnt = sk_GENERAL_NAME_num (names); if (!namecnt) { GENERAL_NAMES_free (names); return (altnames); } for (idx = 0; idx < namecnt; idx++) { entry = sk_GENERAL_NAME_value (names, idx); if (!entry) continue; if (GEN_DNS == entry->type) { ASN1_STRING_to_UTF8 (&utf8, entry->d.dNSName); if (utf8) len = strlen (cptr = (char*)utf8); else len = strlen (cptr = "ASN1_STRING_to_UTF8()?"); } else len = strlen (cptr = "GENERAL_NAME?"); /* do not double-up on the leading common name */ if (!strcasecmp (cptr, common)) continue; strcat (altnames, " "); altsize += len + 1; altnames = realloc (altnames, altsize); strcat (altnames, cptr); if (utf8) { OPENSSL_free(utf8); utf8 = NULL; } } if (names) GENERAL_NAMES_free (names); if (utf8) OPENSSL_free (utf8); return (altnames); } /*****************************************************************************/ /* List the certificates in the directory. Can be used as a script or CLI report. */ void CertManAdminCert () { int filecnt, fullcnt; char *cptr, *certdir, *services; DIR *dirptr; struct dirent *dentptr; /*********/ /* begin */ /*********/ certdir = strdup(wucmeDir()); OpenSSL_add_all_algorithms(); if (wucmeMode (IS_SCRIPT)) { ScriptRecordHeader(); fprintf (stdout, "%s: %s %s\n", tstamp(NULL), SoftwareId, UtilImageName()); } else fprintf (stdout, "%%WUCME-I-CERT, %s %s\n", SoftwareId, UtilImageName()); dirptr = opendir (certdir); if (!dirptr) { warnx ("opendir() %s:%d %%X%08.08X", FI_LI, vaxc$errno); if (wucmeMode (IS_SCRIPT)) EXIT_FI_LI (vaxc$errno); exit (vaxc$errno); } filecnt = fullcnt = 0; for (dentptr = readdir(dirptr); dentptr; dentptr = readdir(dirptr)) { /* only list the wuCME variety */ filecnt++; if (strncasecmp (dentptr->d_name, "wucme_c_", 8)) continue; for (cptr = dentptr->d_name; *cptr; cptr++); while (cptr > dentptr->d_name && *cptr != '.') cptr--; if (strcasecmp (cptr, ".pem")) continue; fullcnt++; cptr = doasprintf ("%s/%s", certdir, dentptr->d_name); CertManCheck (cptr, &services); fflush (stdout); free (cptr); } closedir (dirptr); if (!filecnt) fprintf (stdout, "No file(s) found.\n"); else fprintf (stdout, "%d file(s), %d certificate(s)\n", filecnt, fullcnt); free (certdir); } /*****************************************************************************/ /* Spawn a subprocess to provide a server command to load the certificate(s). If the logical name is not defined and the server version is later than v11.1.0 then an internally defined certificate (re)load is attempted. WUCME_LOAD can be a multi-valued logical name in which case multiple loads are performed in successive subprocesses. If the logical name does exist then that command is executed. $ DEFINE /SYSTEM /EXECUTIVE WUCME_LOAD "@here:submit_to_batch.com" If the logical value begins with a "+" then the value is appended to the internal load command. The string following the plus can be further piped commands. So this example additionally enables CMKRNL (depends on INSTALL with SETPRV) and executes a command procedures. $ DEFINE /SYSTEM /EXECUTIVE WUCME_LOAD - "+ ; set process /privilege=CMKRNL ; @here:do_this.com" If only a "+" as the first of a multi-valued logical name then the internal load commands occur in a subprocess and then additional values each in its own subprocess, as in this example. $ DEFINE /SYSTEM /EXECUTIVE WUCME_LOAD "+","@here:do_this.com" */ void CertManLoad (void) { static ulong flags = 0x02; /* NOCLISYM */ static char piped [] = "pipe set process /privilege=SYSPRV ; " "httpd = \"$wasd_exe:httpd_ssl.exe\" ; " "httpd /do=ssl=cert=load /all"; static $DESCRIPTOR (SubPrcNamDsc, "wuCME-load"); static $DESCRIPTOR (CmdDsc, ""); static $DESCRIPTOR (OutDsc, ""); int index, status, pid, spout, ServerSoftware, SubPrcStatus; char *aptr, *cptr, *sptr, *zptr; char buf [512]; /*********/ /* begin */ /*********/ ServerSoftware = wucmeServerSoftware(); if (!ServerSoftware) { warnx ("manually reload/restart server to load new certificate(s)"); return; } /* if defined subprocess output DOES NOT go into the bit-bucket */ spout = (UtilSysTrnLnm (WUCME_SPOUT) != NULL); for (index = 0; index <= 127; index++) { if (!(cptr = UtilTrnLnm ("WUCME_LOAD", "LNM$SYSTEM", index))) if (index == 0) if (!ServerSoftware || ServerSoftware >= 110100) /* v11.1.0 */ cptr = piped; else { warnx ("restart server to load new certificate(s)"); break; } if (!cptr) break; if (*cptr == '+') { zptr = (sptr = buf) + sizeof(buf)-1; for (aptr = piped; *aptr && sptr < zptr; *sptr++ = *aptr++); for (++cptr; *cptr && sptr < zptr; *sptr++ = *cptr++); *sptr = '\0'; CmdDsc.dsc$a_pointer = cptr = buf; CmdDsc.dsc$w_length = sptr - buf; } else { CmdDsc.dsc$a_pointer = cptr; CmdDsc.dsc$w_length = strlen(cptr); } if (!spout && cptr == piped) { /* for internal load subprocess output goes into the bit-bucket */ OutDsc.dsc$a_pointer = "NL:"; OutDsc.dsc$w_length = 3; } SubPrcStatus = 0; status = lib$spawn (&CmdDsc, 0, &OutDsc, &flags, &SubPrcNamDsc, &pid, &SubPrcStatus, 0, 0, 0, 0, 0, 0, 0); if (status & 1) status = SubPrcStatus; if (status & 1) warnx ("load succeeded using %s", cptr == piped ? "internal commands" : cptr); else warnx ("load failed (%s%08.08X %s) using %s", "%X", status, UtilGetMsg(status), cptr); } } /*****************************************************************************/ /* Convert an ASN1 time object into a string. */ int CertManAsn1time ( ASN1_TIME *t1, char* buf, int blen ) { int rc; BIO *bmem; /*********/ /* begin */ /*********/ bmem = BIO_new(BIO_s_mem()); rc = ASN1_TIME_print(bmem, t1); if (rc <= 0) { BIO_free(bmem); return (-1); } rc = BIO_gets (bmem, buf, blen); if (rc <= 0) { BIO_free(bmem); return (-1); } BIO_free(bmem); return (0); } /*****************************************************************************/ /* Only a single, proctored certificate manager process should run per node (with multiple instances) and only one per cluster with shared WASD configuration. This function instantiates a suitable lock and only returns when this process is granted exclusive access. This process then is allowed to perform certificate management. Requires SYSLCK privilege. */ void CertManLock (char *lname) { int status; char LockName [32]; $DESCRIPTOR (NameDsc, ""); lksb LockSB; /*********/ /* begin */ /*********/ /* in a cluster WUCME_ACTIVE forces wuCME to manage certs on each node */ if (lname) NameDsc.dsc$a_pointer = lname; else if (SyiClusterMember && !wucmeActive) NameDsc.dsc$a_pointer = "WUCME_CLUSTER"; else sprintf (NameDsc.dsc$a_pointer = LockName, "WUCME_%s", SyiNodeName); NameDsc.dsc$w_length = strlen(NameDsc.dsc$a_pointer); memset (&LockSB, 0, sizeof(LockSB)); /* basic lock */ status = sys$enqw (0, LCK$K_NLMODE, &LockSB,LCK$M_EXPEDITE | LCK$M_SYSTEM, &NameDsc, 0, 0, 0, 0, 0, 2, 0); if (status & 1) status = LockSB.lksb$w_status; if (!(status & 1)) EXIT_FI_LI (status); /* convert to a CR lock */ status = sys$enqw (0, LCK$K_CRMODE, &LockSB, LCK$M_CONVERT | LCK$M_SYSTEM, 0, 0, 0, 0, 0, 0, 2, 0); if (status & 1) status = LockSB.lksb$w_status; if (!(status & 1)) EXIT_FI_LI (status); /* queue up for our turn to be the cert manager */ status = sys$enqw (0, LCK$K_EXMODE, &LockSB, LCK$M_CONVERT | LCK$M_SYSTEM, 0, 0, 0, 0, 0, 0, 2, 0); if (status & 1) status = LockSB.lksb$w_status; if (!(status & 1)) EXIT_FI_LI (status); } /*****************************************************************************/