/*****************************************************************************/ #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-2019 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 --------------- 06-JAN-2019 MGD minor changes at v2.0 30-MAR-2018 MGD WCME_AGREEMENT logical name 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 "overseer.h" #include "report.h" #include "script.h" #include "util.h" #include "acme-client/wcme_config.h" #define FI_LI "CERTMAN", __LINE__ static char Utility [] = "WCME"; static char *CgiPlusEsc, *CgiPlusEot; extern int verbose, WcmeIsWASD, WcmeVeryVerbose; extern char *WcmeLogFileName; extern char SoftwareId[]; char *CertManAgreement; 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 */ /*********/ /* 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=ca")) { /* have the netproc access the CA API and display the /directory */ CertManCertify (NULL); 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, "list")) { /* run the standalone http-01 challenge server */ CertManAdminCert (); 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); /* if this is defined use the supplied URL as the TOS */ if (cptr = (UtilTrnLnm ("WCME_AGREEMENT", "LNM$SYSTEM", 0))) { CertManAgreement = strdup(cptr); vmsdbg ("%s", CertManAgreement); } /* 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 */ /*********/ if (UtilInteractive()) fprintf (stdout, "%s\n", (char*)UtilVmsName(cname,-1)); else 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; if (UtilInteractive()) fprintf (stdout, "%s !before: %s !after: %s expires: %d renew: %d\n", altnames, notbefore, notafter, dday, dday - rdays); else 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); } /*****************************************************************************/ /* */ char* CertManAuthorityApi () { static char *apiptr; char *cptr; /*********/ /* begin */ /*********/ if (apiptr) { free (apiptr); apiptr = NULL; } /* if this is defined use the specified URL (for development) */ cptr = UtilTrnLnm ("WCME_AUTHORITY_API", "LNM$SYSTEM", 0); if (!cptr) cptr = CERTMAN_API_ACMEV01; apiptr = strdup (cptr); return (apiptr); } /*****************************************************************************/ /* 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 [2048]; /*********/ /* begin */ /*********/ /* if this is defined use the supplied URL as the TOS */ if (cptr = (UtilTrnLnm ("WCME_AGREEMENT", "LNM$SYSTEM", 0))) { CertManAgreement = strdup(cptr); vmsdbg ("%s", CertManAgreement); } /* 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++); if (sptr < zptr) *sptr++ = '\"'; for (cptr = " ; wcme "; *cptr && sptr < zptr; *sptr++ = *cptr++); if (verbose > 1 || WcmeVeryVerbose) for (cptr = " /verbose"; *cptr && sptr < zptr; *sptr++ = *cptr++); if (services) { for (cptr = " /names=\""; *cptr && sptr < zptr; *sptr++ = *cptr++); cptr = services; /* /force and /revoke can be tacked onto this string (see main())*/ while (*cptr && *cptr != '/') { while (*cptr && (isspace(*cptr) || *cptr == ',')) cptr++; if (!*cptr || *cptr == '/') break; if (cptr > services && sptr < zptr) *sptr++ = ','; while (*cptr && !isspace(*cptr) && *cptr != ',' && sptr < zptr) *sptr++ = *cptr++; } if (sptr < zptr) *sptr++ = '\"'; } *sptr = '\0'; if (!strncasecmp (cptr, "/force", 3)) for (cptr = " /force"; *cptr && sptr < zptr; *sptr++ = *cptr++); if (!strncasecmp (cptr, "/revoke", 3)) for (cptr = " /revoke"; *cptr && sptr < zptr; *sptr++ = *cptr++); CmdLineDsc.dsc$a_pointer = CmdLine; CmdLineDsc.dsc$w_length = sptr - CmdLine; 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); } /*****************************************************************************/ /* List the certificates in the [SSL] directory. Can be used as a script or CLI report. */ 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(); if (UtilInteractive()) fprintf (stdout, "%%WCME-I-CERT, %s\n", OverseerVersion()); else fprintf (stdout, "%s%s\n", stream200, OverseerVersion()); UtilSysPrv(); dirptr = opendir (certdir); if (!dirptr) { vmsdbg ("opendir() %s:%d %%X%08.08X", FI_LI, vaxc$errno); if (UtilInteractive()) exit (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 (*cptr == '/') cptr++; if (strncasecmp (cptr, "cert_", 5) && strncasecmp (cptr, "fullchain_", 10)) return (0); kname = strdup (PRIV_KEY); if (!kname) return (-1); kfp = fopen (kname, "r"); status = vaxc$errno; if (!kfp) { vmsdbg ("fopen() %s:%d %%X%08.08X %s", FI_LI, status, kname); free (kname); return (-1); } cfp = fopen (cname, "a"); status = vaxc$errno; 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); } /*****************************************************************************/