/*****************************************************************************/ /* wuCME.c Pronounced "wack-mee" (perhaps with a slight "wooack"). wuCME wraps the "uacme" ACME v2 implementation by Nicola Di Lieto, licensed and distributed under GPLv3. https://github.com/ndilieto/uacme https://github.com/ndilieto/uacme/blob/master/COPYING wuCME uses uacme to provide the core of the required functionality, with code modifications for use on [Open]VMS, as well as additional code specifically for WASD web server TLS service certificate management. wuCME is a contraction of "WASD u Certificate Management Environment". The "u" in uacme is not explained but is assumed to be a mu as used for micro and meaning small. wuCME supercedes the functionality of the earlier wCME application. Unlike wCME, wuCME is tailored for WASD. Apache now has the mod_md module for ACME. 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 LOGICAL NAMES ------------- All must be defined /SYSTEM and can be /EXECUTIVE. WUCME_ACTIVE * this system's wuCME process is always active WUCME_CHALLEGE preferred challenge (e.g. "http-01") WUCME_DAYS number of days out from expiry renewal occurs WUCME_HERE certificate and keys located here WUCME_LOAD override internal certificate load commands WUCME_MAIL specify email addresses for notification WUCME_NO_DAILY * disables daily certificate management WUCME_OPCOM specify OPCOM target(s) for notification WUCME_STAGING * if defined use staging URL (i.e. --staging) WUCME_VERBOSE * make wuCME rather chatty (i.e. --verbose) INSTALLed PRIVILEGES -------------------- SYSPRV required to access files in [LOCAL] SYSLCK instantiate the wuCME standby/active lock SETPRV OPTIONAL if a WUCME_LOAD command requires something other/more than SYSPRV SHARE required for ALPN-TLS-01 access to BGnnnn VERSION HISTORY --------------- 20-SEP-2024 MGD v2.0.0, ALPN-TLS-01 (RFC8737) acme-tls/1 14-SEP-2024 MGD v1.1.9, bugfix; UACME.C authorize() decline challenge /wasd_root/local to /wasd_local 15-JAN-2023 MGD v1.1.8, CRYPTO.C is_ip() made more "reliable" 24-OCT-2021 MGD v1.1.7, logical name WUCME_ACTIVE for when a clustered system has an independent WASD installation 01-MAR-2021 MGD v1.1.6, wucmeChallenge() use LNM$SYSCLUSTER Happy Birthday second-born 06-JAN-2021 MGD v1.1.5, HttpsVerifyConnect() less specific, more adaptable Happy Birthday first-born 03-AUG-2020 MGD v1.1.4, bugfix; wucmeProbe80() use hostname parameter many happy returns Philip 10-JUL-2020 MGD v1.1.3, wucmeProbe80() use WWW_SERVER_NAME not localhost 04-JUL-2020 MGD v1.1.2, ScriptAdmin() WATCHing advice on authorisation UtilAdjustPriv() refine privilege reporting 17-JUN-2020 MGD v1.1.1, ScriptBegin() refine script activation handle multiple proctored instances (even if silly) 03-JUN-2020 MGD v1.1.0, allow INSTALLed with SETPRV for WUCME_LOAD enhance CertManLoad() for WUCME_LOAD 01-JUN-2020 MGD v1.0.0, initial release 08-AUG-2019 MGD initial development */ /*****************************************************************************/ #define SOFTWAREVN "2.0.0" #define SOFTWARENM "WUCME" /* version of uacme */ #define PACKAGE_VERSION "1.1.2" #ifdef __ALPHA # define SOFTWAREID SOFTWARENM " AXP-" SOFTWAREVN #endif #ifdef __ia64 # define SOFTWAREID SOFTWARENM " IA64-" SOFTWAREVN #endif #ifdef __VAX # error VAX not implemented #endif #ifdef __x86_64 # define SOFTWAREID SOFTWARENM " X86-" SOFTWAREVN #endif #include #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 "WUCME", __LINE__ int dbug, wucmeActive, wucmeScript; char *argv0; uchar SyiClusterMember; char SyiNodeName [16]; char SoftwareId [] = SOFTWAREID, UacmeVersion [] = PACKAGE_VERSION; extern uchar SyiClusterMember; extern char SyiNodeName[], UacmeVersion[]; /*****************************************************************************/ /* Various activities at the wuCME command-line. */ void wucmeCli (int argc, char *argv[]) { int argsc, count, force = 0, retval; char *cptr; #define MAX_ARGSV 128 char *argsv [MAX_ARGSV]; /*********/ /* begin */ /*********/ /* initialise the arguments to wuacme mainline - just in case! */ memset (argsv, argsc = 0, sizeof(argsv)); argsv[argsc++] = argv0; argsv[argsc++] = "--uacme"; if (UtilSysTrnLnm (WUCME_VERBOSE)) argsv[argsc++] = "--verbose"; for (count = 1; count < argc; count++) { cptr = argv[count]; if (!strcasecmp (cptr, "issue")) { if (force) argsv[argsc++] = "--force"; argsv[argsc++] = "issue"; for (count++; count < argc; count++) { if (argsc >= MAX_ARGSV) exit (SS$_BUGCHECK); cptr = argv[count]; while (*cptr) { while (*cptr && *cptr == ',') cptr++; if (*cptr) argsv[argsc++] = cptr; while (*cptr && *cptr != ',') cptr++; if (*cptr) *cptr++ = '\0'; } } retval = mainline (argsc, argsv); exit (retval); } if (!strcasecmp (cptr, "check")) { if (++count < argc) cptr = argv[count]; else cptr = ""; if (!strcasecmp (cptr, "/ca")) { argsv[argsc++] = "ping"; retval = mainline (argsc, argsv); exit (retval); } if (!strcasecmp (cptr, "/http01")) { /* spawn the standalone http-01 challenge server */ Http01Spawn (-999); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "/load")) { /* run the certificate (re)load at the command line */ CertManLoad (); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "/log")) { /* output the current proctored cert manager log */ wucmeCheckLog (); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "/mail")) { /* test the (e)mail notification */ if (cptr = UtilSysTrnLnm (WUCME_MAIL)) cptr = strdup(cptr); else exit (SS$_NOLOGNAM); ReportMail (MAIL_PERSONAL, cptr, "wucme test only!", "wucme test of MAIL report..."); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "/opcom")) { /* test the OPCOM notification */ int target; if (cptr = UtilSysTrnLnm (WUCME_OPCOM)) target = ReportOpcomTargetOf(cptr); else exit (SS$_NOLOGNAM); ReportOpcom (target, "wucme test of OPCOM report..."); exit (SS$_NORMAL); } exit (SS$_BADPARAM); } if (!strcasecmp (cptr, "/force") || !strcasecmp (cptr, "--force")) { force = 1; continue; } if (!strncasecmp (cptr, "certificates", 4)) { /* list all the hosts being certified */ CertManAdminCert (); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "deactivate")) { argsv[argsc++] = "deactivate"; retval = mainline (argsc, argsv); exit (retval); } if (!strncasecmp (cptr, "http01", 6)) { /* create the http-01 responder server */ Http01Begin (cptr); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "manage")) { /* run the certificate management activity at the command line */ CertManBegin (); exit (SS$_NORMAL); } if (!strcasecmp (cptr, "new") || !strcasecmp (cptr, "register")) { argsv[argsc++] = "new"; /* optional email address */ if (count < argc) argsv[argsc++] = argv[++count]; retval = mainline (argsc, argsv); exit (retval); } if (!strcasecmp (cptr, "ping")) { argsv[argsc++] = "ping"; retval = mainline (argsc, argsv); exit (retval); } if (!strcasecmp (cptr, "revoke")) { argsv[argsc++] = "revoke"; if (count < argc) argsv[argsc++] = argv[++count]; retval = mainline (argsc, argsv); exit (retval); } if (!strncasecmp (cptr, "/test=", 6)) { cptr += 6; if (!strcasecmp (cptr, "lock")) { /* execute two (mor more) different sessions ^Y one of them */ printf ("try CertManLock()\n"); CertManLock ("WUCME-TEST"); printf ("got CertManLock() ^Y then $ EXIT when ready\n"); sleep (300); } exit (1); } if (!strcasecmp (cptr, "--version") || !strcasecmp (cptr, "/version")) { fprintf (stdout, "%%WUCME-I-VERSION, %s (%s) (%s) %s\n", SoftwareId, UacmeVersion, OpenSSL_version(OPENSSL_VERSION), argv0); exit (SS$_NORMAL); } /* more detail */ if (!strcasecmp (cptr, "--verbose") || !strcasecmp (cptr, "/verbose")) { argsv[argsc++] = "--verbose"; continue; } exit (SS$_BADPARAM); } } /*****************************************************************************/ /* Return true if the argument matches the current mode, false if not. */ int wucmeMode (int is) { static int mode; char *raptr, *saptr; if (mode) return (mode == is); /* if not a script environment at all */ if (!(getenv ("WWW_SERVER_SOFTWARE") || getenv ("SERVER_SOFTWARE"))) return ((mode = IS_CLI) == is); /* check for proctor instantiation */ if (!(raptr = getenv ("WWW_REMOTE_ADDR"))) raptr = getenv ("REMOTE_ADDR"); if (!(saptr = getenv ("WWW_SERVER_ADDR"))) saptr = getenv ("SERVER_ADDR"); /* should have both these CGI variables */ if (!raptr || !saptr) return ((mode = IS_CLI) == is); /* if either contain a value then not proctored script */ if (*raptr || *saptr) return ((mode = IS_SCRIPT) == is); /* otherwise proctored! */ return ((mode = IS_PROCTOR) == is); } /*****************************************************************************/ /* Get required system data. */ void wucmeGetSyi (void) { static ushort NodeNameLength; static struct { short BufferLength; short ItemCode; void *BufferPtr; void *LengthPtr; } SyiItemList[] = { { sizeof(SyiClusterMember), SYI$_CLUSTER_MEMBER, &SyiClusterMember, 0 }, { sizeof(SyiNodeName), SYI$_NODENAME, &SyiNodeName, &NodeNameLength }, {0,0,0,0} }; int status; $DESCRIPTOR (NameDsc, ""); /*********/ /* begin */ /*********/ status = sys$getsyiw (0, 0, 0, &SyiItemList, 0, 0, 0); if (!(status & 1)) EXIT_FI_LI (status); SyiNodeName[NodeNameLength] = '\0'; } /*****************************************************************************/ /* This function is proctored into operation by ScriptBegin() and manages the day-to-day certificate renewal. Being proctored it is under server control. To prevent the server from deleting the script timeouts are disabled. The proctored script never returns from being activated and so the server will never attempt to use it to process a request. Just sits there, quietly doing its job. */ void wucmeBegin (void) { static int PrevDay; int count, secs, status, startup, PerHour, PollSecs; char *NewLog; ulong BinTime [2]; ushort NumTime [7]; char *cptr; /*********/ /* begin */ /*********/ /* tell the server to leave wuCME alone */ ScriptCallout ("!LIFETIME: DO-NOT-DISTURB\n"); /* avoid proctor contention */ for (count = 0; count < 10; count++) { if (UtilSetPrn ("wuCME-standby")) break; sleep (1); } if (count >= 10) { /* now it's really silly but prevent proctor from thrashing */ char prcnam [16]; for (count = 2; count < 99; count++) { sprintf (prcnam, "wuCME-standby%d", count); if (UtilSetPrn (prcnam)) break; } /* now it's just rediculous */ if (count >= 100) sys$delprc (0, 0); } UtilAdjustPriv(); /* only the one active per system/cluster */ CertManLock (NULL); sleep (1); if (!UtilSetPrn ("wuCME-active")) sys$delprc (0, 0); /* only intended for development purposes */ if (cptr = UtilSysTrnLnm (WUCME_POLL)) { PollSecs = atoi(cptr); if (PollSecs <= 0 || PollSecs > 3600) PollSecs = 60; } else PollSecs = 0; for (startup = 1;;startup = 0) { sys$gettim (&BinTime); sys$numtim (&NumTime, &BinTime); /* open the log */ NewLog = wucmeLog (1); if (startup) warnx ("%s %s", SoftwareId, UtilImageName()); if (!PrevDay) warnx ("starting"); else if (NewLog) warnx ("running"); /* only intended for development purposes */ PerHour = UtilSysTrnLnm (WUCME_HOURLY) != NULL; if ((PrevDay && PrevDay != NumTime[2]) || PerHour) { /* day has changed so perform certificate management */ if (UtilSysTrnLnm (WUCME_NO_DAILY)) warnx ("certificate management disabled by " "logical name WUCME_NO_DAILY"); else { warnx ("certificate management"); CertManBegin (); /* reload the time in case of extended certificate activities */ sys$gettim (&BinTime); sys$numtim (&NumTime, &BinTime); } } if (NumTime[3] || NumTime[4] >= 20) { /* polls at twenty minutes after each hour */ PrevDay = NumTime[2]; secs = (3600 - (NumTime[4] * 60)) + (60 - NumTime[5]) + (19 * 60); } else { /* except if starting after midnight but before twenty minutes past */ PrevDay = -1; secs = (20 * 60) - (NumTime[4] * 60) - (60 - NumTime[5]); } /* close the log */ wucmeLog (0); if (PollSecs) { PrevDay = -1; sleep (PollSecs); } else sleep (secs); } /* should never */ exit (SS$_BUGCHECK); } /*****************************************************************************/ /* Really just intended to normalise multiple periods in a file specification. Also ensures 39.39 compliance. */ int wucme2Ods2 (char *spec) { char *aptr, *cptr, *sptr, *tptr; /*********/ /* begin */ /*********/ if (dbug) printf ("wucme2Ods2() %d |%s|\n", strlen(spec), spec); for (cptr = aptr = sptr = spec; *cptr; cptr++) { if (*cptr == '/' || *cptr == '-' || *cptr == '$' || isalnum(*cptr)) { *sptr++ = *cptr; if (*cptr == '/') aptr = cptr; } else if (*cptr == '.') { /* reduce multiple periods */ for (tptr = cptr+1; *tptr && *tptr != '.' && *tptr != '/'; tptr++); if (*tptr == '.') *sptr++ = '_'; else *sptr++ = *(aptr = cptr); } else *sptr++ = '_'; /* if exceeds 39.39 just undo the last copy */ if (cptr > aptr+39) sptr--; } *sptr = '\0'; if (dbug) printf ("%d |%s|\n", sptr - spec, spec); return (sptr - spec); } /*****************************************************************************/ /* Manage the HTTP01 challenge key and token. Store using a logical name. The token in index 0 and the key in index 1. If a token is not supplied delete the logical name. If the key is not supplied then translate the logical name and check the token values match and return the key. Logical name does not exist or tokens not matched return NULL. If both are supplied the create the logical name containing the token and key in index 0 and 1. Return the key if successful or NULL if unsucessful. */ char* wucmeChallenge (char *token, char *key) { static char LogName [64]; static $DESCRIPTOR (LogNameDsc, LogName); static $DESCRIPTOR (LnmClusterDsc, "LNM$SYSCLUSTER"); static ushort lenkey, lentoken; static ulong lattr0, lattr1; static ulong lindex0 = 0, lindex1 = 1; static char lkey [256], ltoken [256]; static struct { short int buf_len; short int item; void *buf_addr; ushort *ret_len; } CreLnmItems [] = { { 0, LNM$_STRING, 0, 0 }, { 0, LNM$_STRING, 0, 0 }, { 0,0,0,0 } }, TrnLnmItems [] = { { sizeof(lindex0), LNM$_INDEX, &lindex0, 0 }, { sizeof(lattr0), LNM$_ATTRIBUTES, &lattr0, 0 }, { sizeof(ltoken)-1, LNM$_STRING, ltoken, &lentoken }, { sizeof(lindex1), LNM$_INDEX, &lindex1, 0 }, { sizeof(lattr1), LNM$_ATTRIBUTES, &lattr1, 0 }, { sizeof(lkey)-1, LNM$_STRING, lkey, &lenkey }, { 0,0,0,0 } }; int len, status; /*********/ /* begin */ /*********/ if (!LogName[0]) { if (SyiClusterMember && !wucmeActive) len = sprintf (LogName, "%s_CLUSTER", WUCME_CHALLENGE); else len = sprintf (LogName, "%s_%s", WUCME_CHALLENGE, SyiNodeName); LogNameDsc.dsc$w_length = len; } if (!token) { /* delete logical name */ sys$dellnm (&LnmClusterDsc, &LogNameDsc, 0); LogName[0] = '\0'; return (NULL); } if (!strcmp (token, WUCME_ALPN1_TOKEN)) { /* this token returns the key (tls-alpn-01) */ status = sys$trnlnm (0, &LnmClusterDsc, &LogNameDsc, 0, &TrnLnmItems); if (!(status & 1)) return (NULL); if (!(lattr0 & LNM$M_EXISTS)) return (NULL); if (!(lattr1 & LNM$M_EXISTS)) return (NULL); /* return the key */ lkey[lenkey] = '\0'; return (lkey); } else if (!strcmp (token, WUCME_PROBE_TOKEN)) { /* this faux token is used to probe port 80 for a challenge response */ return (WUCME_PROBE_TOKEN); } else if (!key) { /* translate the name, compare the token, return the key */ status = sys$trnlnm (0, &LnmClusterDsc, &LogNameDsc, 0, &TrnLnmItems); if (!(status & 1)) return (NULL); if (!(lattr0 & LNM$M_EXISTS)) return (NULL); if (!(lattr1 & LNM$M_EXISTS)) return (NULL); ltoken[lentoken] = '\0'; /* if the supplied key does not match */ if (strcmp (token, ltoken)) return (NULL); /* return the key */ lkey[lenkey] = '\0'; return (lkey); } CreLnmItems[0].buf_len = strlen(token); CreLnmItems[0].buf_addr = token; CreLnmItems[1].buf_len = strlen(key); CreLnmItems[1].buf_addr = key; status = sys$crelnm (0, &LnmClusterDsc, &LogNameDsc, 0, &CreLnmItems); if (status & 1) return (key); return (NULL); } /*****************************************************************************/ /* Ensure there is a port 80 listener for processing the challenge responses. */ void wucmeChallenge80 (const char *ident) { int rcode; /*********/ /* begin */ /*********/ /* this logical name disables the listener (for development purposes) */ if (UtilSysTrnLnm (WUCME_NO_HTTP01)) return; /* probe port 80 for challenge responses */ rcode = wucmeProbe80 (ident, 5); if (rcode <= 0) { /* port 80 was not contactable */ warnx ("FAILED to connect to port 80 service"); warnx ("deploying internal port 80 responder"); Http01Spawn (1); } else if (rcode != 200) warnx("challenge probe failed %d", rcode); } /*****************************************************************************/ /* Probe port 80 to check if it is available for challenge responses. */ int wucmeProbe80 (const char *ident, int retry) { int rcode; char *cptr, *sptr, *zptr; char buf [256]; /*********/ /* begin */ /*********/ zptr = (sptr = buf) + sizeof(buf)-1; for (cptr = "http://"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = (char*)ident; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = ":80"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = WUCME_PROBE_CHALLENGE; *cptr && sptr < zptr; *sptr++ = *cptr++); *sptr = '\0'; while (retry > 0) { sleep (1); rcode = httpsGetRequest (buf, NULL); if (rcode > 0) break; retry--; } return (rcode); } /*****************************************************************************/ /* Manage the detached (proctored script process) log file. */ char* wucmeLog (int logit) { static char fname [256]; int retval; ulong BinTime [2]; ushort NumTime [7]; char *cptr, *sptr, *zptr ; FILE *fp; struct stat stat_buf; /*********/ /* begin */ /*********/ if (!logit) { /* close the log file (by reopening to NL:) */ stderr = freopen ("/nl", "w", stderr); if (!stderr) EXIT_FI_LI (vaxc$errno); return (NULL); } sys$gettim (&BinTime); sys$numtim (&NumTime, &BinTime); if (!(cptr = UtilSysTrnLnm (WUCME_LOGS))) cptr = "wasd_server_logs:"; sprintf (fname, "%swucme_%04.04d%02.02d.log", cptr, NumTime[0], NumTime[1]); /* just return the current log file name */ if (logit < 0) return (fname); retval = stat (fname, &stat_buf); stderr = freopen (fname, "a", stderr, "ctx=rec", "rfm=var", "rat=cr", "rop=rlk", "shr=get", "shr=put"); if (!stderr) EXIT_FI_LI (vaxc$errno); /* returning the file name indicates the file did not previously exist */ if (retval) return (fname); return (NULL); } /*****************************************************************************/ /* Return an integer representing the server software version. "HTTPd-WASD/11.5.1" should become 110501. Assign a global symbol for use at the command-line. */ int wucmeServerSoftware (void) { static int ServerSoftware; char *cptr; if (ServerSoftware) return (ServerSoftware); if (!(cptr = getenv ("WWW_SERVER_SOFTWARE"))) if (!(cptr = getenv ("SERVER_SOFTWARE"))) /* at CLI then no automatic load */ return (ServerSoftware = 0); while (*cptr && !isdigit(*cptr)) cptr++; ServerSoftware = atoi(cptr) * 10000; while (*cptr && isdigit(*cptr)) cptr++; if (*cptr == '.') cptr++; ServerSoftware += atoi(cptr) * 100; while (*cptr && isdigit(*cptr)) cptr++; if (*cptr == '.') cptr++; ServerSoftware += atoi(cptr); return (ServerSoftware); } /*****************************************************************************/ /* Report the current "wuCME-active" process log. */ void wucmeCheckLog (void) { char line [4096]; char *cptr; FILE *fp; /*********/ /* begin */ /*********/ fp = fopen (cptr = wucmeLog(-1), "r", "shr=put"); if (!fp) { warnx ("open %s failed: %s", cptr, strerror(errno)); return; } while (fgets (line, sizeof(line), fp)) fputs (line, stdout); fclose (fp); } /*****************************************************************************/ /* Simply replaces the regex calls in the original uacme.c. */ char* wucmeFindHeader (const char *headers, const char *name) { char *aptr, *cptr, *hptr, *sptr; /*********/ /* begin */ /*********/ for (hptr = (char*)headers; *hptr; hptr++) { if (!isalpha (*hptr)) continue; if (tolower(*hptr) != tolower(*name)) continue; cptr = hptr; for (aptr = (char*)name; *aptr; aptr++, cptr++) if (tolower(*aptr) != tolower(*cptr) || *cptr == ':') break; if (*aptr || *cptr != ':') continue; for (cptr++; *cptr && *cptr == ' '; cptr++); for (sptr = cptr; *cptr && *cptr != '\r' && *cptr != '\n'; cptr++); if (*cptr == '\r' || *cptr == '\n') break; } if (!*hptr) return (NULL); aptr = hptr = calloc (1, cptr - sptr + 1); while (sptr < cptr) *aptr++ = *sptr++; return (hptr); } /*****************************************************************************/ /* Return the location of the server certificates and keys. Logical name value can be be defined as VMS or Unix-style syntax. */ char* wucmeDir (void) { static char *dptr = NULL; /*********/ /* begin */ /*********/ if (dptr) return (dptr); if (dptr = UtilSysTrnLnm (WUCME_HERE)) { if (*dptr != '/') dptr = decc$translate_vms (dptr); return (dptr); } else return (dptr = "/wasd_local"); } /*****************************************************************************/ /* Output the argument array to (the log file). */ void wucmeArgcArgv (int argc, char *argv[]) { /*********/ /* begin */ /*********/ fprintf (stderr, "%s: (%d)", tstamp(NULL), argc); for (int acnt = 0; acnt < argc; acnt++) fprintf (stderr, " %s", argv[acnt]); fputs ("\n", stderr); } /*****************************************************************************/