/*****************************************************************************/ #ifdef COMMENTS_WITH_COMMENTS /* certman.c Certificate management functions. Interfaces (spawns) with acme-client-portable to do all the interesting stuff. COPYRIGHT --------- Copyright (C) 2017 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 --------------- 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 "certman.h" #include "http01.h" #include "report.h" #include "script.h" #include "util.h" #include "acmeport/config.h" #define FI_LI "CERTMAN", __LINE__ static char Utility [] = "WCME"; static int CertManStaging; static char *CgiPlusEsc, *CgiPlusEot; extern int WcmeIsWASD, WcmeVeryVerbose; extern char *AcmePortEsPtr, *WcmeLogFileName; extern char SoftwareId[]; char* doasprintf (char*, ...); /*****************************************************************************/ /* Various activities at the command-line. These of course perform the activity under the account used at the CLI. Also see ScriptAdmin() for checks available under the scripting account. */ void CertManCLI (char *what) { char *cptr; /*********/ /* begin */ /*********/ AcmePortEsPtr = "cli"; /* only for development purposes */ WcmeVeryVerbose = (UtilTrnLnm ("WCME_VERBOSE", "LNM$SYSTEM", 0) != NULL); if (!strncasecmp (what, "certify=", 8)) { /* get certificates for the supplied comma-separated list of hosts */ CertManCertify (what+8); exit (SS$_NORMAL); } if (!strcasecmp (what, "check=http01")) { /* spawn the standalone http-01 challenge server */ Http01Spawn (-999); exit (SS$_NORMAL); } if (!strcasecmp (what, "check=load")) { /* run the certificate (re)load at the command line */ CertManLoad (); exit (SS$_NORMAL); } if (!strcasecmp (what, "check=mail")) { /* test the (e)mail notification */ if (cptr = UtilTrnLnm ("WCME_MAIL", "LNM$SYSTEM", 0)) cptr = strdup(cptr); else exit (SS$_NOLOGNAM); ReportMail (MAIL_PERSONAL, cptr, "wCME test only!", "wCME test of MAIL report..."); exit (SS$_NORMAL); } if (!strcasecmp (what, "check=opcom")) { /* test the OPCOM notification */ int target; if (cptr = UtilTrnLnm ("WCME_OPCOM", "LNM$SYSTEM", 0)) target = ReportOpcomTargetOf(cptr); else exit (SS$_NOLOGNAM); ReportOpcom (target, "wCME test of OPCOM report..."); exit (SS$_NORMAL); } if (!strcasecmp (what, "http01")) { /* run the standalone http-01 challenge server */ Http01Begin (); exit (SS$_NORMAL); } if (!strcasecmp (what, "manage")) { /* run the certificate management activity at the command line */ CertManBegin (); exit (SS$_NORMAL); } if (!strncasecmp (what, "detach=", 7)) { int status; ulong pid; char *input, *output, *pname, *uname; /* detach=,[],[],[] */ for (input = cptr = what = strdup(what+7); *cptr && *cptr != ','; cptr++); if (*cptr) *cptr++ = '\0'; for (output = cptr; *cptr && *cptr != ','; cptr++); if (*cptr) *cptr++ = '\0'; for (uname = cptr; *cptr && *cptr != ','; cptr++); if (*cptr) *cptr++ = '\0'; for (pname = cptr; *cptr && *cptr != ','; cptr++); if (*cptr) *cptr++ = '\0'; status = UtilCrePrcDetach (uname, input, output, pname, &pid); if (status & 1) fprintf (stdout, "%%WCME-I-PID, %08.08X\n", pid); exit (status); } if (!strcasecmp (what, "version")) { fprintf (stdout, "%%WCME-I-VERSION, %s\n", SoftwareId); exit (SS$_NORMAL); } exit (SS$_BADPARAM); } /*****************************************************************************/ /* 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 () { int certcnt, failcnt, filecnt, http01, renew, spawned01, succnt, status; char *cptr, *certdir, *fname, *services; DIR *dirptr; struct dirent *dentptr; /*********/ /* begin */ /*********/ /* only for development purposes */ WcmeVeryVerbose = (UtilTrnLnm ("WCME_VERBOSE", "LNM$SYSTEM", 0) != NULL); /* define system-level logical to use the Let's Encrypt devlopment site */ if (CertManStaging = (UtilTrnLnm ("WCME_STAGING", "LNM$SYSTEM", 0) != NULL)) vmsdbg ("%s", CERTMAN_STAGE_CA); /* default port is 80 but for test/development can be run on alternate */ http01 = (UtilTrnLnm ("WCME_HTTP01", "LNM$SYSTEM", 0) != NULL); certdir = strdup(SSL_DIR); OpenSSL_add_all_algorithms(); UtilSysPrv(); dirptr = opendir (certdir); if (!dirptr) { vmsdbg ("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, "fullchain_", 10)) 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) { UtilMereMortal(); if (!spawned01 && (spawned01 = http01)) Http01Spawn (0); status = CertManCertify (services); UtilSysPrv(); if (status & 1) { succnt++; cptr = doasprintf ("wCME successful renewal\n%s\n%s", (char*)UtilVmsName(fname,-1), services); } else { failcnt++; /* bug in C-RTL? %%X%08.08X fails to produce correct output */ cptr = doasprintf ("wCME FAILED renewal\n%s%08.08X %s\n%s\n%s", "%%X", status, UtilGetMsg(status), (char*)UtilVmsName(fname,-1), services); } ReportThis (cptr); free (cptr); } free (fname); } closedir (dirptr); UtilMereMortal(); free (certdir); if (spawned01) Http01Spawn (1); if (succnt) CertManLoad (); vmsdbg ("%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 */ /*********/ vmsdbg ("%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 = UtilTrnLnm ("WCME_DAYS", "LNM$SYSTEM", 0)) { /* 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) { vmsdbg ("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); vmsdbg ("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))) { vmsdbg ("FromAsn1time(X509_get_notBefore()) failed"); goto out; } if (CertManAsn1time (asn1aft, notafter, sizeof(notafter))) { vmsdbg ("FromAsn1time(X509_get_notAfter()) failed"); goto out; } if (!ASN1_TIME_diff (&dday, &dsec, NULL, asn1aft)) { vmsdbg ("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; vmsdbg ("%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); } /*****************************************************************************/ /* 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); } /*****************************************************************************/ /* Spawn an acme-client-portable command-line process to request a certificate for the specified service(s). Service list can be either comma or space separated. Enables SYSPRV to allow the spawned (script) process to execute WCME. PIPE was introduced with VMS V7.1 so this is the minimum supported version. Return a VMS status code. */ int CertManCertify (char *services) { static ulong flags = 0x02; /* NOCLISYM => no WWW_.. symbols */ static $DESCRIPTOR (CmdLineDsc, ""); int status, pid, SubPrcStatus; char *cptr, *sptr, *zptr; char CmdLine [1024]; /*********/ /* begin */ /*********/ /* if this is defined use the Let's Encrypt development site */ if (CertManStaging = (UtilTrnLnm ("WCME_STAGING", "LNM$SYSTEM", 0) != NULL)) vmsdbg ("%s", CERTMAN_STAGE_CA); /* only intended for development purposes */ WcmeVeryVerbose = (UtilTrnLnm ("WCME_VERBOSE", "LNM$SYSTEM", 0) != NULL); zptr = (sptr = CmdLine) + sizeof(CmdLine)-1; for (cptr = "pipe set process/privilege=sysprv"; *cptr && sptr < zptr; *sptr++ = *cptr++); if (WcmeLogFileName) { for (cptr = " ; define/process WCME_LOG_FILE \""; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = WcmeLogFileName; *cptr && sptr < zptr; *sptr++ = *cptr++); if (sptr < zptr) *sptr++ = '\"'; } for (cptr = " ; wcme=\"$"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = UtilImageName(); *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = "\" ; wcme \"-nNv"; *cptr && sptr < zptr; *sptr++ = *cptr++); if (WcmeVeryVerbose && sptr < zptr) *sptr++ = 'v'; if (CertManStaging && sptr < zptr) *sptr++ = 's'; if (sptr < zptr) *sptr++ = '\"'; cptr = services; while (*cptr) { while (*cptr && (isspace(*cptr) || *cptr == ',')) cptr++; if (!*cptr) break; if (sptr < zptr) *sptr++ = ' '; while (*cptr && !isspace(*cptr) && *cptr != ',' && sptr < zptr) *sptr++ = *cptr++; } *sptr = '\0'; CmdLineDsc.dsc$a_pointer = CmdLine; CmdLineDsc.dsc$w_length = sptr - CmdLine; vmsdbg ("$ %s", strstr(CmdLine,"wcme ")); UtilSysPrv(); status = lib$spawn (&CmdLineDsc, 0, 0, &flags, 0, &pid, &SubPrcStatus, 0, 0, 0, 0, 0, 0, 0); UtilMereMortal(); if (status & 1) status = SubPrcStatus; CertManHouseKeeper (); return (status); } /*****************************************************************************/ /* Return an HTTP response listing the certificates in the [SSL] directory. */ void CertManAdminCert () { extern char stream200[]; int filecnt, fullcnt; char *cptr, *certdir, *services; DIR *dirptr; struct dirent *dentptr; /*********/ /* begin */ /*********/ certdir = strdup(SSL_DIR); OpenSSL_add_all_algorithms(); fprintf (stdout, "%s%s\n", stream200, SoftwareId); UtilSysPrv(); dirptr = opendir (certdir); if (!dirptr) { vmsdbg ("opendir() %s:%d %%X%08.08X", FI_LI, vaxc$errno); EXIT_FI_LI (vaxc$errno); } filecnt = fullcnt = 0; for (dentptr = readdir(dirptr); dentptr; dentptr = readdir(dirptr)) { /* only list the fullchain variety */ filecnt++; if (strncasecmp (dentptr->d_name, "fullchain_", 10)) 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); free (cptr); } closedir (dirptr); UtilMereMortal(); if (!filecnt) fprintf (stdout, "No file(s) found.\n"); else fprintf (stdout, "%d file(s), %d fullchain certificate(s)\n", filecnt, fullcnt); free (certdir); } /*****************************************************************************/ /* Append the private key to the specified certificate file. Called by [.acme-client-portable...]fileproc.c after serialising cert file. Return 0 for success, -1 for error. */ int CertManAppendPrivKey (char *cname) { int status; char *cptr, *kname; char line [256]; FILE *cfp, *kfp; /*********/ /* begin */ /*********/ for (cptr = cname; *cptr; cptr++); while (cptr > cname && *cptr != '/') cptr--; if (strncasecmp (cptr, "/cert_", 6) && strncasecmp (cptr, "/fullchain_", 11)) return (0); kname = strdup (SSL_PRIV_DIR "/" PRIVKEY_FILE); if (!kname) return (vaxc$errno); UtilSysPrv(); kfp = fopen (kname, "r"); status = vaxc$errno; UtilMereMortal(); if (!kfp) { vmsdbg ("fopen() %s:%d %%X%08.08X %s", FI_LI, status, kname); free (kname); return (-1); } UtilSysPrv(); cfp = fopen (cname, "a"); status = vaxc$errno; UtilMereMortal(); if (!cfp) { vmsdbg ("fopen() %s:%d %%X%08.08X %s", FI_LI, status, cname); fclose (kfp); free (kname); return (-1); } while (fgets (line, sizeof(line), kfp)) { if (fputs (line, cfp) < 0) { vmsdbg ("puts() %s:%d %%X%08.08X %s", FI_LI, vaxc$errno, cname); fclose (cfp); fclose (kfp); free (kname); return (-1); } } fclose (cfp); fclose (kfp); free (kname); return (0); } /*****************************************************************************/ /* If the logical name WCME_CERT exists then copy the specified certificate into that directory. Can be multi-valued in which case it is copied to multiple destinations. Return 1..127 for success, 0 no action, -1 for error. */ int CertManInstall (char *certname) { int index, status; char *cptr, *instdir, *instname; char line [256]; FILE *cfp, *ifp; /*********/ /* begin */ /*********/ /* excise the directory, just use the name */ for (cptr = certname; *cptr; cptr++); while (cptr > certname && *cptr != '/') cptr--; if (cptr > certname) cptr++; /* only install the fullchain variety */ if (strncasecmp (cptr, "fullchain_", 10)) return (0); for (index = 0; index <= 127; index++) { if (!(instdir = UtilTrnLnm ("WCME_CERT", "LNM$SYSTEM", index))) break; instname = doasprintf ("%s%s", instdir, cptr); UtilSysPrv(); cfp = fopen (certname, "r"); status = vaxc$errno; UtilMereMortal(); if (!cfp) { vmsdbg ("fopen() %s:%d %%X%08.08X %s", FI_LI, status, certname); free (instname); return (-1); } UtilSysPrv(); ifp = fopen (instname, "w"); status = vaxc$errno; UtilMereMortal(); if (!ifp) { vmsdbg ("fopen() %s:%d %%X%08.08X %s", FI_LI, status, instname); fclose (cfp); free (instname); return (-1); } while (fgets (line, sizeof(line), cfp)) { if (fputs (line, ifp) < 0) { vmsdbg ("puts() %s:%d %%X%08.08X %s", FI_LI, vaxc$errno, certname); fclose (cfp); fclose (ifp); free (instname); return (-1); } } vmsdbg ("installed %s", instname); fclose (cfp); fclose (ifp); free (instname); } return (index); } /*****************************************************************************/ /* Spawn a subprocess to provide a server command to load the certificates. Can be a multi-valued logical name in which case multiple loads are performed. */ void CertManLoad () { static ulong flags = 0x02; /* NOCLISYM */ static $DESCRIPTOR (SubPrcNamDsc, "WCME-load"); static $DESCRIPTOR (CmdDsc, ""); int index, status, pid, SubPrcStatus; char *cptr; /*********/ /* begin */ /*********/ for (index = 0; index <= 127; index++) { if (!(cptr = UtilTrnLnm ("WCME_LOAD", "LNM$SYSTEM", index))) break; CmdDsc.dsc$a_pointer = cptr; CmdDsc.dsc$w_length = strlen(cptr); SubPrcStatus = 0; UtilSysPrv(); /* VMS Apache requires IMPERSONATE to restart */ UtilImpersonate(); status = lib$spawn (&CmdDsc, 0, 0, &flags, &SubPrcNamDsc, &pid, &SubPrcStatus, 0, 0, 0, 0, 0, 0, 0); UtilMereMortal(); if (status & 1) status = SubPrcStatus; if (status & 1) vmsdbg ("load succeeded using %s", cptr); else vmsdbg ("load failed (%s%08.08X %s) using %s", "%X", status, UtilGetMsg(status), cptr); } } /*****************************************************************************/ /* Cleanup any challenge files left around after a failed attempt. */ void CertManHouseKeeper () { int chngcnt, status; char *fname, *wwwdir; DIR *dirptr; struct dirent *dentptr; /*********/ /* begin */ /*********/ wwwdir = strdup(WWW_DIR); UtilSysPrv(); dirptr = opendir (wwwdir); if (!dirptr) { vmsdbg ("opendir() %s:%d %%X%08.08X", FI_LI, vaxc$errno); UtilMereMortal(); return; } chngcnt = 0; for (dentptr = readdir(dirptr); dentptr; dentptr = readdir(dirptr)) { chngcnt++; fname = doasprintf ("%s/%s", wwwdir, dentptr->d_name); while (!remove (fname)); vmsdbg ("deleted challenge %s", UtilVmsName(fname,-1)); free (fname); } closedir (dirptr); UtilMereMortal(); free (wwwdir); } /*****************************************************************************/ /* 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); } /*****************************************************************************/