/*****************************************************************************/ /* Sesola[A]cme.c Activate a CGI agent to service this particular Application-Layer Protocol Negotiation. Initially intended for ACME's alpn-tls-01 protocol https://letsencrypt.org/docs/challenge-types/ https://datatracker.ietf.org/doc/html/rfc7301 This functionality is activated from SesolaHelloCallback(). Simple test case: openssl s_client -debug -connect :443 -servername -alpn acme-tls/1 Processing can be WATCHed using [x]CGI and [x]DCL along with a *special case* RX trigger of "acme-tls/1". VERSION HISTORY --------------- 16-SEP-2024 MGD process a configured ALPN response as a DCL script */ /*****************************************************************************/ #ifdef WASD_VMS_V7 #undef _VMS__V6__SOURCE #define _VMS__V6__SOURCE #undef __VMS_VER #define __VMS_VER 70000000 #undef __CRTL_VER #define __CRTL_VER 70000000 #endif /* standard C header files */ #include #include #include #include /* VMS related header files */ #include #include /* application header files */ #define SESOLA_REQUIRED #include "Sesola.h" #define WASD_MODULE "SESOLACME" /***************************************/ #ifdef SESOLA /* secure sockets layer */ /***************************************/ /******************/ /* global storage */ /******************/ char ProblemAlpnAgent [] = "Problem initiating ALPN agent"; #define ALPN_AGENT_ACTIVE_MAX 8 #define ALPN_AGENT_BUSY_MAX 100 static int SesolaAcmeAgentActiveCount, SesolaAcmeAgentActiveMax = ALPN_AGENT_ACTIVE_MAX, SesolaAcmeAgentBusyMax = ALPN_AGENT_BUSY_MAX, SesolaAcmeAgentBusyCount, SesolaAcmeAgentBusyLimit; /********************/ /* external storage */ /********************/ extern char SoftwareID[]; extern const int64 Delta100mSec; extern CONFIG_STRUCT Config; extern HTTPD_PROCESS HttpdProcess; extern WATCH_STRUCT Watch; /*****************************************************************************/ /* This function is called during the early stages of client hello processing on the server to determine whether the ALPN is TLS-ALPN-01 ("acme-tls/1"). https://datatracker.ietf.org/doc/html/rfc8737 */ int SesolaAcmeTls1 ( SSL *SslPtr, int *al, void *arg ) { int nlen, retval, status, extLen; char *cptr, *czptr, *sptr, *zptr; unsigned char *extData; SESOLA_STRUCT *sesolaptr; /*********/ /* begin */ /*********/ if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeTls1()"); sesolaptr = (SESOLA_STRUCT*)SSL_get_ex_data (SslPtr, 0); /* if already discovered the protocol */ if (sesolaptr->AlpnProtocol) return (SSL_CLIENT_HELLO_SUCCESS); /* get the ALPN */ retval = SSL_client_hello_get0_ext (SslPtr, TLSEXT_TYPE_application_layer_protocol_negotiation, &extData, &extLen); if (retval != 1) return (SSL_CLIENT_HELLO_SUCCESS); czptr = (cptr = extData) + extLen; if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchDataDump (cptr, czptr - cptr); cptr += sizeof(ushort); /* provide an ALPN extension with the single protocol name "acme-tls/1" */ if (cptr < czptr) { nlen = (uint)(uchar*)*cptr++; if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL !#AZ", nlen, nlen, cptr); if (!strncmp (cptr, "acme-tls/1", nlen)) sesolaptr->AlpnProtocol = "acme-tls/1"; cptr += nlen; } /* if not acme-tls/1 or not alone */ if (!sesolaptr->AlpnProtocol || cptr < czptr) return (SSL_CLIENT_HELLO_SUCCESS); /* continue on to the get server name */ retval = SSL_client_hello_get0_ext (SslPtr, TLSEXT_TYPE_server_name, &extData, &extLen); if (retval != 1) return (SSL_CLIENT_HELLO_ERROR); if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL", extLen); czptr = (cptr = extData) + extLen; if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchDataDump (cptr, czptr - cptr); cptr += sizeof(ushort); if (cptr >= czptr) return (SSL_CLIENT_HELLO_ERROR); if (*(USHORTPTR)cptr != TLSEXT_NAMETYPE_host_name) return (SSL_CLIENT_HELLO_ERROR); cptr += sizeof(ushort); if (cptr < czptr) { nlen = (uint)(uchar*)*cptr++; if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!UL !#AZ", nlen, nlen, cptr); zptr = (sptr = sesolaptr->SNIServerName) + sizeof(sesolaptr->SNIServerName)-1; while (*cptr && sptr < zptr) *sptr++ = *cptr++; *sptr = '\0'; if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!AZ", sesolaptr->SNIServerName); } /* if acme-tls/1 with no server name */ if (!sesolaptr->SNIServerName[0]) return (SSL_CLIENT_HELLO_ERROR); if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "!AZ !AZ", sesolaptr->AlpnProtocol, sesolaptr->SNIServerName); /* set up the agent's operating parameters */ sesolaptr->AlpnAgentAstFunction = SesolaEndAlpnAgent; sesolaptr->AlpnAgentAstParam = sesolaptr; status = SesolaAcmeAgentBegin (sesolaptr); if (VMSnok (status)) { if (WATCHMOD (sesolaptr, WATCH_MOD_SESOLA)) WatchThis (WATCHITM(sesolaptr), WATCH_MOD_SESOLA, "SesolaAcmeAgentBegin() !&S", status); return (SSL_CLIENT_HELLO_ERROR); } return (SSL_CLIENT_HELLO_RETRY); } /*****************************************************************************/ /* Activate an agent (script) to perform the the ACME certificate generation used used, in this case by wuCME. ASYNCHRONOUS! An ACME agent does not have an associated request, it creates a faux one! */ int SesolaAcmeAgentBegin (void *vptr) { int status; char *cptr, *sptr, *zptr; char AgentScript [128]; REQUEST_STRUCT *rqptr; SESOLA_STRUCT *sesolaptr; /*********/ /* begin */ /*********/ if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentBegin()"); sesolaptr = (SESOLA_STRUCT*)vptr; if (!sesolaptr->AlpnAgentAstFunction) { /* no AST supplied then an error */ return (SS$_ABORT); } if (SesolaAcmeAgentActiveCount >= SesolaAcmeAgentActiveMax) { /* busy, busy, busy ... requeue for a delayed subsequent attempt */ if (SesolaAcmeAgentBusyCount++ <= SesolaAcmeAgentBusyMax) { if (SesolaAcmeAgentBusyCount <= SesolaAcmeAgentBusyMax / 4) SysDclAst (SesolaAcmeAgentBegin, sesolaptr); else sys$setimr (0, &Delta100mSec, SesolaAcmeAgentBegin, sesolaptr, 0); return (SS$_NORMAL); } else { ErrorNoticed (NULL, SS$_TOOMANYREDS, "!UL exceeded (!UL)", FI_LI, SesolaAcmeAgentBusyMax, SesolaAcmeAgentBusyLimit); SesolaAcmeAgentBusyCount++; sesolaptr->AlpnAgentStatus = SS$_ABORT; SysDclAst (sesolaptr->AlpnAgentAstFunction, sesolaptr->AlpnAgentAstParam); return (SS$_ABORT); } } if (SesolaAcmeAgentBusyCount) { if (SesolaAcmeAgentBusyCount > SesolaAcmeAgentBusyLimit) SesolaAcmeAgentBusyLimit = SesolaAcmeAgentBusyCount; SesolaAcmeAgentBusyCount = 0; } /************/ /* continue */ /************/ if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "begin !AZ", sesolaptr->AlpnProtocol); rqptr = DclFauxRequest (NULL, NULL, NULL); rqptr->AgentAstParam = (void*)sesolaptr; rqptr->AgentRequestPtr = VmGetHeap (rqptr, 256); zptr = (sptr = rqptr->AgentRequestPtr) + 255; for (cptr = sesolaptr->SNIServerName; *cptr && sptr < zptr; *sptr++ = *cptr++); if (sptr < zptr) *sptr++ = ' '; /* e.g. "acme-tls/1" becomes "/cgi-bin/acme_tls_1" */ zptr = (sptr = AgentScript) + sizeof(AgentScript)-1; for (cptr = "/cgi-bin/"; *cptr && sptr < zptr; *sptr++ = *cptr++); for (cptr = sesolaptr->AlpnProtocol; *cptr && sptr < zptr; cptr++) if (isalnum (*cptr)) *sptr++ = *cptr; else *sptr++ = '_'; *sptr = '\0'; if (Watch.TriggerRxCount) if (!strcmp (Watch.TriggerRx, sesolaptr->AlpnProtocol)) rqptr->WatchItem = ++Watch.ItemCount; if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "ACME !AZ AGENT !AZ", sesolaptr->AlpnProtocol, AgentScript); rqptr = DclFauxRequest (rqptr, AgentScript, &SesolaAcmeAgentEnd); if (!rqptr) { ErrorNoticed (NULL, SS$_ABORT, ProblemAlpnAgent, FI_LI); sesolaptr->AlpnAgentStatus = SS$_ABORT; SysDclAst (sesolaptr->AlpnAgentAstFunction, sesolaptr->AlpnAgentAstParam); return (SS$_ABORT); } SesolaAcmeAgentActiveCount++; return (SS$_NORMAL); } /*****************************************************************************/ /* The ACME agent (script) has completed. Should have returned OPAQUE: data and then copied into ACME storage, with a format . Continue the TLS handshake. */ void SesolaAcmeAgentEnd (REQUEST_STRUCT *rqptr) { int count, retval, status; char *aptr, *cptr; SESOLA_STRUCT *sesolaptr; SSL *SslPtr; /*********/ /* begin */ /*********/ if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentEnd() 0x!8XL !UL", rqptr->OpaqueDataPtr, rqptr->OpaqueDataCount); sesolaptr = (SESOLA_STRUCT*)rqptr->AgentAstParam; SslPtr = sesolaptr->SslPtr; sesolaptr->AlpnAgentStatus = SS$_NORMAL; if (cptr = rqptr->AgentResponsePtr) { if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME !AZ", cptr); status = atoi(cptr); while (isdigit (*cptr)) cptr++; while (*cptr == ' ') cptr++; if (status >= 200 && status <= 299) { /***********/ /* success */ /***********/ if (rqptr->OpaqueDataPtr) { sesolaptr->AcmeDataPtr = VmGet (rqptr->OpaqueDataCount); memcpy (sesolaptr->AcmeDataPtr, rqptr->OpaqueDataPtr, rqptr->OpaqueDataCount); sesolaptr->AcmeDataCount = rqptr->OpaqueDataCount; } else { ErrorNoticed (NULL, SS$_ABORT, rqptr->AgentResponsePtr, FI_LI); sesolaptr->AlpnAgentStatus = SS$_ABORT; } } else { /**********/ /* failed */ /**********/ while (*cptr && *(USHORTPTR)cptr != '%X') cptr++; if (*cptr) { /* if agent reporting VMS error */ status = strtol (cptr+2, NULL, 16); ErrorNoticed (NULL, status, rqptr->AgentResponsePtr, FI_LI); sesolaptr->AlpnAgentStatus = status; } else { ErrorNoticed (NULL, SS$_ABORT, rqptr->AgentResponsePtr, FI_LI); sesolaptr->AlpnAgentStatus = SS$_ABORT; } } } else { ErrorNoticed (NULL, SS$_ABORT, ProblemAlpnAgent, FI_LI); sesolaptr->AlpnAgentStatus = SS$_ABORT; } if (SesolaAcmeAgentActiveCount) SesolaAcmeAgentActiveCount--; if (WATCHING (rqptr, WATCH_SESOLA)) WatchThis (WATCHITM(rqptr), WATCH_SESOLA, "ACME !AZ !&S", sesolaptr->AlpnProtocol, sesolaptr->AlpnAgentStatus); SysDclAst (sesolaptr->AlpnAgentAstFunction, sesolaptr->AlpnAgentAstParam); /* remove from any supervisory list */ HttpdSupervisorList (rqptr, -1); VmFreeRequest (rqptr, FI_LI); if (VMSok (sesolaptr->AlpnAgentStatus)) { retval = SSL_do_handshake (SslPtr); if (retval <= 0) { if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME handshake failed !SL", retval); status = SS$_ABORT; } } else { /* "close" the SSL connection */ SesolaNetBeginFail (sesolaptr); } } /*****************************************************************************/ /* The agent (script) should have returned OPAQUE: data and then copied into ACME storage, with a format . The data is reconstructed into certificate and private key and then set for this connection for when the handshake is finalised. Called from SesolaAlpnCallback(). */ int SesolaAcmeCertify (SESOLA_STRUCT *sesolaptr) { int crtlen, keylen, retval, status; char *bptr, *bzptr, *crtptr, *keyptr; SSL *SslPtr; /*********/ /* begin */ /*********/ if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeCertify() 0x!8XL !UL", sesolaptr->AcmeDataPtr, sesolaptr->AcmeDataCount); SslPtr = sesolaptr->SslPtr; if (!sesolaptr->AcmeDataPtr) return (SS$_BADPARAM); crtptr = keyptr = NULL; crtlen = keylen = 0; bzptr = (bptr = sesolaptr->AcmeDataPtr) + sesolaptr->AcmeDataCount; if (bptr + 2 < bzptr) { /* get certificate length and certificate itself */ crtlen = *(USHORTPTR)bptr; bptr += 2; if (bptr + crtlen < bzptr) { crtptr = bptr; bptr += crtlen; } if (bptr + 2 < bzptr) { /* get key length and key itself */ keylen = *(USHORTPTR)bptr; bptr += 2; if (bptr + keylen <= bzptr) keyptr = bptr; } } status = SS$_NORMAL; if (crtlen + keylen + sizeof(ushort) + sizeof(ushort) != sesolaptr->AcmeDataCount) { if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME sanity !UL !UL !UL", sesolaptr->AcmeDataCount, crtlen, keylen); status = SS$_BADPARAM; } if (!crtptr || !keyptr) { if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME sanity 0x!8XL 0x!8XL", crtptr, keyptr); status = SS$_BADPARAM; } if (VMSok (status)) { retval = SSL_use_certificate_ASN1 (SslPtr, crtptr, crtlen); if (!retval) { if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME cert failed"); status = SS$_ABORT; } } if (VMSok (status)) { retval = SSL_use_PrivateKey_ASN1(EVP_PKEY_EC, SslPtr, keyptr, keylen); if (!retval) { if (WATCH_MODULE (WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_SESOLA, "ACME key failed"); status = SS$_ABORT; } } return (status); } /*****************************************************************************/ /* For compilations without SSL these functions provide LINKage stubs for the rest of the HTTPd modules, allowing for just recompiling the Sesola module to integrate the SSL functionality. */ /*********************/ #else /* not SESOLA */ /*********************/ /* external storage */ extern WATCH_STRUCT Watch; int SesolaAcmeAgentBegin (void *vptr) { if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentBegin()"); return (SS$_ABORT); } void SesolaAcmeAgentEnd (REQUEST_STRUCT *rqptr) { if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeAgentEnd()"); } void SesolaAcmeCertify (void *ptr) { if (WATCH_MODULE(WATCH_MOD_SESOLA)) WatchThis (WATCHALL, WATCH_MOD_SESOLA, "SesolaAcmeCertify()"); } /************************/ #endif /* ifdef SESOLA */ /************************/ /*****************************************************************************/